[{"data":1,"prerenderedAt":1907},["ShallowReactive",2],{"docsv3-nav":3,"\u002Fdocs\u002Fv3\u002Ffundamentals\u002Fsignup-protection":198},[4],{"title":5,"path":6,"stem":7,"children":8,"page":188},"V3","\u002Fdocs\u002Fv3","1.docs\u002Fv3",[9,13,17,21,38,87,189],{"title":10,"path":11,"stem":12},"Introduction","\u002Fdocs\u002Fv3\u002Fintroduction","1.docs\u002Fv3\u002F1.Introduction",{"title":14,"path":15,"stem":16},"Quick start","\u002Fdocs\u002Fv3\u002Fquick-start","1.docs\u002Fv3\u002F2.Quick start",{"title":18,"path":19,"stem":20},"Challenge flow","\u002Fdocs\u002Fv3\u002Fchallenge-flow","1.docs\u002Fv3\u002F3.Challenge flow",{"title":22,"path":23,"stem":24,"children":25},"Fundamentals","\u002Fdocs\u002Fv3\u002Ffundamentals","1.docs\u002Fv3\u002F4.fundamentals",[26,30,34],{"title":27,"path":28,"stem":29},"Signup protection","\u002Fdocs\u002Fv3\u002Ffundamentals\u002Fsignup-protection","1.docs\u002Fv3\u002F4.fundamentals\u002F00.Signup protection",{"title":31,"path":32,"stem":33},"Login protection","\u002Fdocs\u002Fv3\u002Ffundamentals\u002Flogin-protection","1.docs\u002Fv3\u002F4.fundamentals\u002F01.Login protection",{"title":35,"path":36,"stem":37},"Access protection","\u002Fdocs\u002Fv3\u002Ffundamentals\u002Faccess-protection","1.docs\u002Fv3\u002F4.fundamentals\u002F02.Access protection",{"title":39,"path":40,"stem":41,"children":42},"Guides","\u002Fdocs\u002Fv3\u002Fguides","1.docs\u002Fv3\u002F5.guides",[43,47,51,55,59,63,67,71,75,79,83],{"title":44,"path":45,"stem":46},"Account sharing prevention","\u002Fdocs\u002Fv3\u002Fguides\u002Faccount-sharing-prevention","1.docs\u002Fv3\u002F5.guides\u002F1.Account sharing prevention",{"title":48,"path":49,"stem":50},"Web scraping prevention","\u002Fdocs\u002Fv3\u002Fguides\u002Fweb-scraping-prevention","1.docs\u002Fv3\u002F5.guides\u002F13.Web scraping prevention",{"title":52,"path":53,"stem":54},"Ban enforcement","\u002Fdocs\u002Fv3\u002Fguides\u002Fban-enforcement","1.docs\u002Fv3\u002F5.guides\u002F14.Ban enforcement",{"title":56,"path":57,"stem":58},"Chargeback dispute","\u002Fdocs\u002Fv3\u002Fguides\u002Fchargeback-dispute","1.docs\u002Fv3\u002F5.guides\u002F15.Chargeback dispute",{"title":60,"path":61,"stem":62},"Multi-accounting prevention","\u002Fdocs\u002Fv3\u002Fguides\u002Fmulti-accounting-prevention","1.docs\u002Fv3\u002F5.guides\u002F16.Multi-accounting prevention",{"title":64,"path":65,"stem":66},"Account takeover prevention","\u002Fdocs\u002Fv3\u002Fguides\u002Faccount-takeover-prevention","1.docs\u002Fv3\u002F5.guides\u002F2.Account takeover prevention",{"title":68,"path":69,"stem":70},"Risky transaction prevention","\u002Fdocs\u002Fv3\u002Fguides\u002Frisky-transaction-prevention","1.docs\u002Fv3\u002F5.guides\u002F20.Risky transaction prevention",{"title":72,"path":73,"stem":74},"Fake account detection","\u002Fdocs\u002Fv3\u002Fguides\u002Ffake-account-detection","1.docs\u002Fv3\u002F5.guides\u002F3.Fake account detection",{"title":76,"path":77,"stem":78},"Bot detection","\u002Fdocs\u002Fv3\u002Fguides\u002Fbot-detection","1.docs\u002Fv3\u002F5.guides\u002F4.Bot detection",{"title":80,"path":81,"stem":82},"Card testing prevention","\u002Fdocs\u002Fv3\u002Fguides\u002Fcard-testing-prevention","1.docs\u002Fv3\u002F5.guides\u002F5.Card testing prevention",{"title":84,"path":85,"stem":86},"Incentive abuse prevention","\u002Fdocs\u002Fv3\u002Fguides\u002Fincentive-abuse-prevention","1.docs\u002Fv3\u002F5.guides\u002F9.Incentive abuse prevention",{"title":88,"path":89,"stem":90,"children":91,"page":188},"Concepts","\u002Fdocs\u002Fv3\u002Fconcepts","1.docs\u002Fv3\u002F6.concepts",[92,96,100,104,108,112,116,120,124,128,132,136,140,144,148,152,156,160,164,168,172,176,180,184],{"title":93,"path":94,"stem":95},"Evaluations","\u002Fdocs\u002Fv3\u002Fconcepts\u002Fevaluations","1.docs\u002Fv3\u002F6.concepts\u002F01.evaluations",{"title":97,"path":98,"stem":99},"Actions","\u002Fdocs\u002Fv3\u002Fconcepts\u002Factions","1.docs\u002Fv3\u002F6.concepts\u002F02.actions",{"title":101,"path":102,"stem":103},"Signals","\u002Fdocs\u002Fv3\u002Fconcepts\u002Fsignals","1.docs\u002Fv3\u002F6.concepts\u002F03.signals",{"title":105,"path":106,"stem":107},"Checks","\u002Fdocs\u002Fv3\u002Fconcepts\u002Fchecks","1.docs\u002Fv3\u002F6.concepts\u002F04.checks",{"title":109,"path":110,"stem":111},"Risks","\u002Fdocs\u002Fv3\u002Fconcepts\u002Frisks","1.docs\u002Fv3\u002F6.concepts\u002F05.risks",{"title":113,"path":114,"stem":115},"Verdicts","\u002Fdocs\u002Fv3\u002Fconcepts\u002Fverdicts","1.docs\u002Fv3\u002F6.concepts\u002F06.verdicts",{"title":117,"path":118,"stem":119},"Policies","\u002Fdocs\u002Fv3\u002Fconcepts\u002Fpolicies","1.docs\u002Fv3\u002F6.concepts\u002F07.policies",{"title":121,"path":122,"stem":123},"Challenges","\u002Fdocs\u002Fv3\u002Fconcepts\u002Fchallenges","1.docs\u002Fv3\u002F6.concepts\u002F08.challenges",{"title":125,"path":126,"stem":127},"Concurrency","\u002Fdocs\u002Fv3\u002Fconcepts\u002Fconcurrency","1.docs\u002Fv3\u002F6.concepts\u002F09.concurrency",{"title":129,"path":130,"stem":131},"Impossible travel","\u002Fdocs\u002Fv3\u002Fconcepts\u002Fimpossible-travel","1.docs\u002Fv3\u002F6.concepts\u002F10.impossible-travel",{"title":133,"path":134,"stem":135},"Bots","\u002Fdocs\u002Fv3\u002Fconcepts\u002Fbots","1.docs\u002Fv3\u002F6.concepts\u002F11.bots",{"title":137,"path":138,"stem":139},"Devices","\u002Fdocs\u002Fv3\u002Fconcepts\u002Fdevices","1.docs\u002Fv3\u002F6.concepts\u002F12.devices",{"title":141,"path":142,"stem":143},"Fingerprints","\u002Fdocs\u002Fv3\u002Fconcepts\u002Ffingerprints","1.docs\u002Fv3\u002F6.concepts\u002F13.fingerprints",{"title":145,"path":146,"stem":147},"People","\u002Fdocs\u002Fv3\u002Fconcepts\u002Fpeople","1.docs\u002Fv3\u002F6.concepts\u002F14.people",{"title":149,"path":150,"stem":151},"Lists","\u002Fdocs\u002Fv3\u002Fconcepts\u002Flists","1.docs\u002Fv3\u002F6.concepts\u002F15.lists",{"title":153,"path":154,"stem":155},"Account takeover","\u002Fdocs\u002Fv3\u002Fconcepts\u002Faccount-takeover","1.docs\u002Fv3\u002F6.concepts\u002F16.account-takeover",{"title":157,"path":158,"stem":159},"Account sharing","\u002Fdocs\u002Fv3\u002Fconcepts\u002Faccount-sharing","1.docs\u002Fv3\u002F6.concepts\u002F17.account-sharing",{"title":161,"path":162,"stem":163},"Fake account","\u002Fdocs\u002Fv3\u002Fconcepts\u002Ffake-account","1.docs\u002Fv3\u002F6.concepts\u002F18.fake-account",{"title":165,"path":166,"stem":167},"Scraping","\u002Fdocs\u002Fv3\u002Fconcepts\u002Fscraping","1.docs\u002Fv3\u002F6.concepts\u002F19.scraping",{"title":169,"path":170,"stem":171},"Linked accounts","\u002Fdocs\u002Fv3\u002Fconcepts\u002Flinked-accounts","1.docs\u002Fv3\u002F6.concepts\u002F20.linked-accounts",{"title":173,"path":174,"stem":175},"New IP","\u002Fdocs\u002Fv3\u002Fconcepts\u002Fip","1.docs\u002Fv3\u002F6.concepts\u002F21.ip",{"title":177,"path":178,"stem":179},"Anonymizing network","\u002Fdocs\u002Fv3\u002Fconcepts\u002Fanonymizing-network","1.docs\u002Fv3\u002F6.concepts\u002F22.anonymizing-network",{"title":181,"path":182,"stem":183},"Email quality","\u002Fdocs\u002Fv3\u002Fconcepts\u002Femail","1.docs\u002Fv3\u002F6.concepts\u002F23.email",{"title":185,"path":186,"stem":187},"Velocity","\u002Fdocs\u002Fv3\u002Fconcepts\u002Fvelocity","1.docs\u002Fv3\u002F6.concepts\u002F24.velocity",false,{"title":190,"path":191,"stem":192,"children":193,"page":188},"Advanced","\u002Fdocs\u002Fv3\u002Fadvanced","1.docs\u002Fv3\u002F7.Advanced",[194],{"title":195,"path":196,"stem":197},"Proxy setup","\u002Fdocs\u002Fv3\u002Fadvanced\u002Fproxy-setup","1.docs\u002Fv3\u002F7.Advanced\u002F1.Proxy-setup",{"id":199,"title":27,"body":200,"description":1901,"extension":1902,"meta":1903,"navigation":358,"path":28,"rawbody":1904,"seo":1905,"stem":29,"__hash__":1906},"docsv3\u002F1.docs\u002Fv3\u002F4.fundamentals\u002F00.Signup protection.md",{"type":201,"value":202,"toc":1892},"minimark",[203,207,220,230,235,238,266,269,273,277,292,296,307,738,742,745,1185,1191,1246,1250,1265,1268,1276,1279,1283,1297,1668,1684,1688,1700,1888],[204,205,27],"h1",{"id":206},"signup-protection",[208,209,210,211,215,216,219],"p",{},"Signup is the awkward action. The user doesn't have an ID yet, so the usual evaluate flow doesn't quite fit — you'd normally pass ",[212,213,214],"code",{},"user"," to Rupt and your server would later check that ",[212,217,218],{},"user.id"," on the evaluation matches. On the first signup there's nothing to match against, and a naive integration ends up letting attackers slip past the challenge by closing the tab and reloading your site.",[208,221,222,223,226,227,229],{},"It builds on the generic ",[224,225,18],"a",{"href":19}," and adds the binding that makes signup safe. Along with ",[224,228,31],{"href":32},", it's one of the two foundations every other guide builds on. Fake account, multi-accounting, and the other signup-time guides point back here because the shape is the same.",[231,232,234],"h2",{"id":233},"what-this-protects-against","What this protects against",[208,236,237],{},"Skip this pattern and you'll see these problems.",[239,240,241,249,260,263],"ul",{},[242,243,244,245,248],"li",{},"A user triggers a challenge, closes the tab, walks to ",[212,246,247],{},"\u002Flogin",". Without server-side state they look like any other new account.",[242,250,251,252,255,256,259],{},"The client sends one email to Rupt and a different one to your ",[212,253,254],{},"\u002Fregister",". Your server sees ",[212,257,258],{},"verdict: allow"," and trusts it.",[242,261,262],{},"Same email, several signup attempts, none challenge-completed, all of them landing in your DB.",[242,264,265],{},"Someone grabs the signup success URL after the challenge completed and replays it, hoping to mint a second session off the same evaluation.",[208,267,268],{},"The flow below closes them.",[231,270,272],{"id":271},"the-flow","The flow",[274,275],"mermaid-diagram",{"code":276},"sequenceDiagram\n  actor U as User\n  participant C as Signup form\n  participant S as Your server\n  participant R as Rupt\n  participant X as Challenge UI\n\n  U->>C: Submits signup form (email, password)\n  C->>R: evaluate.signup({ email })\n  R-->>C: { evaluation_id, redirect? }\n  C->>S: POST \u002Fsignup { form fields, evaluation_id }\n  S->>R: GET \u002Fv3\u002Fevaluations\u002F{evaluation_id}\n  R-->>S: { verdict, user, challenge }\n  Note over S: Integrity check (action, email, verdict)\n\n  alt verdict = deny\n    S-->>C: Reject (401)\n    C-->>U: Show error\n  else verdict = allow\n    S->>S: Create user, signup_status = allowed\n    S-->>C: { token, user }\n    C-->>U: Logged in\n  else verdict = challenge\n    S->>S: Create user, signup_status = await_challenge_completion\n    S-->>C: { redirect }\n    C-->>U: Navigate to challenge URL\n    U->>X: Complete challenge\n    X->>S: Redirect to success_url?evaluation=…\n    S->>R: POST \u002Fv3\u002Fevaluations\u002F{evaluation_id}\u002Fconsume\n    R-->>S: { challenge.status, user } — or 409 if already used\n    Note over S: One shot — consume reads and claims at once,\u003Cbr\u002F>confirm it matches the pending user and the challenge completed\n    S->>S: Flip user.signup_status = complete\n    S-->>U: Issue token, navigate in\n  end\n",[208,278,279,280,283,284,287,288,291],{},"Your server should keep a reference to the evaluation and the status of the signup on the User record. Something like ",[212,281,282],{},"signup_evaluation_id"," and ",[212,285,286],{},"signup_status"," on your ",[212,289,290],{},"User"," model do the trick. We'll fill in the details in the next steps.",[231,293,295],{"id":294},"step-1-call-evaluate-on-the-client","Step 1: Call evaluate on the client",[208,297,298,299,302,303,306],{},"On signup you normally don't have a user ID yet. Pass ",[212,300,301],{},"email"," (and ",[212,304,305],{},"phone"," if you collect it).",[308,309,310,603,679],"client-platform",{},[311,312,314],"template",{"v-slot:web":313},"",[315,316,320],"pre",{"className":317,"code":318,"language":319,"meta":313,"style":313},"language-js shiki shiki-themes material-theme-lighter one-dark-pro monokai","import Rupt from \"@ruptjs\u002Fclient\";\n\nconst rupt = new Rupt({ clientId: \"your_client_id\" });\n\nconst evaluation = await rupt.evaluate.signup({\n  email: form.email,\n  \u002F\u002F phone: form.phone,\n});\n\n\u002F\u002F POST \u002Fsignup to your server with the evaluation ID\nawait fetch(\"YOUR_SIGNUP_ENDPOINT\u002Fsignup\", {\n  method: \"POST\",\n  body: JSON.stringify({\n    ...form,\n    evaluation_id: evaluation?.evaluation_id,\n  }),\n});\n","js",[212,321,322,353,360,411,416,447,465,472,482,487,493,517,534,554,566,584,594],{"__ignoreMap":313},[323,324,327,331,335,338,342,346,349],"span",{"class":325,"line":326},"line",1,[323,328,330],{"class":329},"sAPXc","import",[323,332,334],{"class":333},"seeE2"," Rupt",[323,336,337],{"class":329}," from",[323,339,341],{"class":340},"s9QZx"," \"",[323,343,345],{"class":344},"siibJ","@ruptjs\u002Fclient",[323,347,348],{"class":340},"\"",[323,350,352],{"class":351},"shEKG",";\n",[323,354,356],{"class":325,"line":355},2,[323,357,359],{"emptyLinePlaceholder":358},true,"\n",[323,361,363,367,371,375,379,382,386,389,393,396,398,401,403,406,409],{"class":325,"line":362},3,[323,364,366],{"class":365},"sHm3x","const",[323,368,370],{"class":369},"sZ9uN"," rupt",[323,372,374],{"class":373},"sut_7"," =",[323,376,378],{"class":377},"srTuz"," new",[323,380,334],{"class":381},"sjp9t",[323,383,385],{"class":384},"sJCYa","(",[323,387,388],{"class":351},"{",[323,390,392],{"class":391},"sUwfj"," clientId",[323,394,395],{"class":351},":",[323,397,341],{"class":340},[323,399,400],{"class":344},"your_client_id",[323,402,348],{"class":340},[323,404,405],{"class":351}," }",[323,407,408],{"class":384},")",[323,410,352],{"class":351},[323,412,414],{"class":325,"line":413},4,[323,415,359],{"emptyLinePlaceholder":358},[323,417,419,421,424,426,429,431,434,437,439,442,444],{"class":325,"line":418},5,[323,420,366],{"class":365},[323,422,423],{"class":369}," evaluation",[323,425,374],{"class":373},[323,427,428],{"class":329}," await",[323,430,370],{"class":369},[323,432,433],{"class":351},".",[323,435,436],{"class":369},"evaluate",[323,438,433],{"class":351},[323,440,441],{"class":381},"signup",[323,443,385],{"class":384},[323,445,446],{"class":351},"{\n",[323,448,450,453,455,458,460,462],{"class":325,"line":449},6,[323,451,452],{"class":391},"  email",[323,454,395],{"class":351},[323,456,457],{"class":369}," form",[323,459,433],{"class":351},[323,461,301],{"class":333},[323,463,464],{"class":351},",\n",[323,466,468],{"class":325,"line":467},7,[323,469,471],{"class":470},"s42Qa","  \u002F\u002F phone: form.phone,\n",[323,473,475,478,480],{"class":325,"line":474},8,[323,476,477],{"class":351},"}",[323,479,408],{"class":384},[323,481,352],{"class":351},[323,483,485],{"class":325,"line":484},9,[323,486,359],{"emptyLinePlaceholder":358},[323,488,490],{"class":325,"line":489},10,[323,491,492],{"class":470},"\u002F\u002F POST \u002Fsignup to your server with the evaluation ID\n",[323,494,496,499,502,504,506,509,511,514],{"class":325,"line":495},11,[323,497,498],{"class":329},"await",[323,500,501],{"class":381}," fetch",[323,503,385],{"class":384},[323,505,348],{"class":340},[323,507,508],{"class":344},"YOUR_SIGNUP_ENDPOINT\u002Fsignup",[323,510,348],{"class":340},[323,512,513],{"class":351},",",[323,515,516],{"class":351}," {\n",[323,518,520,523,525,527,530,532],{"class":325,"line":519},12,[323,521,522],{"class":391},"  method",[323,524,395],{"class":351},[323,526,341],{"class":340},[323,528,529],{"class":344},"POST",[323,531,348],{"class":340},[323,533,464],{"class":351},[323,535,537,540,542,545,547,550,552],{"class":325,"line":536},13,[323,538,539],{"class":391},"  body",[323,541,395],{"class":351},[323,543,544],{"class":369}," JSON",[323,546,433],{"class":351},[323,548,549],{"class":381},"stringify",[323,551,385],{"class":384},[323,553,446],{"class":351},[323,555,557,561,564],{"class":325,"line":556},14,[323,558,560],{"class":559},"sKfv_","    ...",[323,562,563],{"class":333},"form",[323,565,464],{"class":351},[323,567,569,572,574,576,579,582],{"class":325,"line":568},15,[323,570,571],{"class":391},"    evaluation_id",[323,573,395],{"class":351},[323,575,423],{"class":369},[323,577,578],{"class":351},"?.",[323,580,581],{"class":333},"evaluation_id",[323,583,464],{"class":351},[323,585,587,590,592],{"class":325,"line":586},16,[323,588,589],{"class":351},"  }",[323,591,408],{"class":384},[323,593,464],{"class":351},[323,595,597,599,601],{"class":325,"line":596},17,[323,598,477],{"class":351},[323,600,408],{"class":384},[323,602,352],{"class":351},[311,604,605],{"v-slot:ios":313},[315,606,610],{"className":607,"code":608,"language":609,"meta":313,"style":313},"language-swift shiki shiki-themes material-theme-lighter one-dark-pro monokai","let response = try await rupt.evaluate(\n  action: \"signup\",\n  email: form.email\n)\n\n\u002F\u002F POST evaluation.evaluationId to your server with the form\n","swift",[212,611,612,638,653,665,670,674],{"__ignoreMap":313},[323,613,614,618,621,624,627,629,632,635],{"class":325,"line":326},[323,615,617],{"class":616},"s2NTT","let",[323,619,620],{"class":384}," response ",[323,622,623],{"class":559},"=",[323,625,626],{"class":329}," try",[323,628,428],{"class":329},[323,630,631],{"class":384}," rupt.",[323,633,436],{"class":634},"sh6BQ",[323,636,637],{"class":351},"(\n",[323,639,640,643,645,647,649,651],{"class":325,"line":355},[323,641,642],{"class":634},"  action",[323,644,395],{"class":351},[323,646,341],{"class":340},[323,648,441],{"class":344},[323,650,348],{"class":340},[323,652,464],{"class":384},[323,654,655,657,659,662],{"class":325,"line":362},[323,656,452],{"class":634},[323,658,395],{"class":351},[323,660,661],{"class":384}," form.",[323,663,664],{"class":333},"email\n",[323,666,667],{"class":325,"line":413},[323,668,669],{"class":351},")\n",[323,671,672],{"class":325,"line":418},[323,673,359],{"emptyLinePlaceholder":358},[323,675,676],{"class":325,"line":449},[323,677,678],{"class":470},"\u002F\u002F POST evaluation.evaluationId to your server with the form\n",[311,680,681],{"v-slot:android":313},[315,682,686],{"className":683,"code":684,"language":685,"meta":313,"style":313},"language-kotlin shiki shiki-themes material-theme-lighter one-dark-pro monokai","val response = rupt.evaluate(\n  action = \"signup\",\n  email = form.email,\n)\n\n\u002F\u002F POST response.evaluationId to your server with the form\n","kotlin",[212,687,688,703,715,725,729,733],{"__ignoreMap":313},[323,689,690,693,695,697,699,701],{"class":325,"line":326},[323,691,692],{"class":377},"val",[323,694,620],{"class":384},[323,696,623],{"class":373},[323,698,631],{"class":384},[323,700,436],{"class":381},[323,702,637],{"class":384},[323,704,705,708,710,713],{"class":325,"line":355},[323,706,707],{"class":384},"  action ",[323,709,623],{"class":373},[323,711,712],{"class":344}," \"signup\"",[323,714,464],{"class":384},[323,716,717,720,722],{"class":325,"line":362},[323,718,719],{"class":384},"  email ",[323,721,623],{"class":373},[323,723,724],{"class":384}," form.email,\n",[323,726,727],{"class":325,"line":413},[323,728,669],{"class":384},[323,730,731],{"class":325,"line":418},[323,732,359],{"emptyLinePlaceholder":358},[323,734,735],{"class":325,"line":449},[323,736,737],{"class":470},"\u002F\u002F POST response.evaluationId to your server with the form\n",[231,739,741],{"id":740},"step-2-handle-the-verdict-on-your-server","Step 2: Handle the verdict on your server",[208,743,744],{},"Your server fetches the evaluation, runs the integrity check (the action and email match what you expected), and then handles the verdict.",[315,746,750],{"className":747,"code":748,"language":749,"meta":313,"style":313},"language-ts shiki shiki-themes material-theme-lighter one-dark-pro monokai","\u002F\u002F POST \u002Fsignup\nconst evaluation = await rupt.getEvaluation(evaluation_id);\n\n\u002F\u002F Integrity check — block tampering before anything else\nif (evaluation.action !== \"signup\") return reject(\"Action mismatch\");\nif (evaluation.user?.email !== form.email) return reject(\"Identity mismatch\");\n\nif (evaluation.verdict === \"deny\") {\n  return reject(\"Signup denied\");\n}\n\nif (evaluation.verdict === \"allow\") {\n  const user = await User.create({\n    ...form,\n    signup_status: \"complete\",\n  });\n  return { token: issueToken(user), user };\n}\n\nif (evaluation.verdict === \"challenge\") {\n  const user = await User.create({\n    ...form,\n    signup_status: \"await_challenge_completion\",\n    signup_evaluation_id: evaluation.id,\n  });\n  return { redirect: evaluation.redirect };\n}\n","ts",[212,751,752,757,782,786,791,838,881,885,912,933,938,942,967,991,999,1015,1023,1051,1056,1061,1087,1108,1117,1133,1150,1159,1180],{"__ignoreMap":313},[323,753,754],{"class":325,"line":326},[323,755,756],{"class":470},"\u002F\u002F POST \u002Fsignup\n",[323,758,759,761,763,765,767,769,771,774,776,778,780],{"class":325,"line":355},[323,760,366],{"class":365},[323,762,423],{"class":369},[323,764,374],{"class":373},[323,766,428],{"class":329},[323,768,370],{"class":369},[323,770,433],{"class":351},[323,772,773],{"class":381},"getEvaluation",[323,775,385],{"class":384},[323,777,581],{"class":333},[323,779,408],{"class":384},[323,781,352],{"class":351},[323,783,784],{"class":325,"line":362},[323,785,359],{"emptyLinePlaceholder":358},[323,787,788],{"class":325,"line":413},[323,789,790],{"class":470},"\u002F\u002F Integrity check — block tampering before anything else\n",[323,792,793,796,799,802,804,807,810,812,814,816,819,822,825,827,829,832,834,836],{"class":325,"line":418},[323,794,795],{"class":329},"if",[323,797,798],{"class":384}," (",[323,800,801],{"class":369},"evaluation",[323,803,433],{"class":351},[323,805,806],{"class":333},"action",[323,808,809],{"class":373}," !==",[323,811,341],{"class":340},[323,813,441],{"class":344},[323,815,348],{"class":340},[323,817,818],{"class":384},") ",[323,820,821],{"class":329},"return",[323,823,824],{"class":381}," reject",[323,826,385],{"class":384},[323,828,348],{"class":340},[323,830,831],{"class":344},"Action mismatch",[323,833,348],{"class":340},[323,835,408],{"class":384},[323,837,352],{"class":351},[323,839,840,842,844,846,848,850,852,854,856,858,860,862,864,866,868,870,872,875,877,879],{"class":325,"line":449},[323,841,795],{"class":329},[323,843,798],{"class":384},[323,845,801],{"class":369},[323,847,433],{"class":351},[323,849,214],{"class":369},[323,851,578],{"class":351},[323,853,301],{"class":333},[323,855,809],{"class":373},[323,857,457],{"class":369},[323,859,433],{"class":351},[323,861,301],{"class":333},[323,863,818],{"class":384},[323,865,821],{"class":329},[323,867,824],{"class":381},[323,869,385],{"class":384},[323,871,348],{"class":340},[323,873,874],{"class":344},"Identity mismatch",[323,876,348],{"class":340},[323,878,408],{"class":384},[323,880,352],{"class":351},[323,882,883],{"class":325,"line":467},[323,884,359],{"emptyLinePlaceholder":358},[323,886,887,889,891,893,895,898,901,903,906,908,910],{"class":325,"line":474},[323,888,795],{"class":329},[323,890,798],{"class":384},[323,892,801],{"class":369},[323,894,433],{"class":351},[323,896,897],{"class":333},"verdict",[323,899,900],{"class":373}," ===",[323,902,341],{"class":340},[323,904,905],{"class":344},"deny",[323,907,348],{"class":340},[323,909,818],{"class":384},[323,911,446],{"class":351},[323,913,914,917,919,922,924,927,929,931],{"class":325,"line":484},[323,915,916],{"class":329},"  return",[323,918,824],{"class":381},[323,920,385],{"class":921},"s2Cpd",[323,923,348],{"class":340},[323,925,926],{"class":344},"Signup denied",[323,928,348],{"class":340},[323,930,408],{"class":921},[323,932,352],{"class":351},[323,934,935],{"class":325,"line":489},[323,936,937],{"class":351},"}\n",[323,939,940],{"class":325,"line":495},[323,941,359],{"emptyLinePlaceholder":358},[323,943,944,946,948,950,952,954,956,958,961,963,965],{"class":325,"line":519},[323,945,795],{"class":329},[323,947,798],{"class":384},[323,949,801],{"class":369},[323,951,433],{"class":351},[323,953,897],{"class":333},[323,955,900],{"class":373},[323,957,341],{"class":340},[323,959,960],{"class":344},"allow",[323,962,348],{"class":340},[323,964,818],{"class":384},[323,966,446],{"class":351},[323,968,969,972,975,977,979,982,984,987,989],{"class":325,"line":536},[323,970,971],{"class":365},"  const",[323,973,974],{"class":369}," user",[323,976,374],{"class":373},[323,978,428],{"class":329},[323,980,981],{"class":369}," User",[323,983,433],{"class":351},[323,985,986],{"class":381},"create",[323,988,385],{"class":921},[323,990,446],{"class":351},[323,992,993,995,997],{"class":325,"line":556},[323,994,560],{"class":559},[323,996,563],{"class":333},[323,998,464],{"class":351},[323,1000,1001,1004,1006,1008,1011,1013],{"class":325,"line":568},[323,1002,1003],{"class":391},"    signup_status",[323,1005,395],{"class":351},[323,1007,341],{"class":340},[323,1009,1010],{"class":344},"complete",[323,1012,348],{"class":340},[323,1014,464],{"class":351},[323,1016,1017,1019,1021],{"class":325,"line":586},[323,1018,589],{"class":351},[323,1020,408],{"class":921},[323,1022,352],{"class":351},[323,1024,1025,1027,1030,1033,1035,1038,1040,1042,1044,1046,1048],{"class":325,"line":596},[323,1026,916],{"class":329},[323,1028,1029],{"class":351}," {",[323,1031,1032],{"class":391}," token",[323,1034,395],{"class":351},[323,1036,1037],{"class":381}," issueToken",[323,1039,385],{"class":921},[323,1041,214],{"class":333},[323,1043,408],{"class":921},[323,1045,513],{"class":351},[323,1047,974],{"class":333},[323,1049,1050],{"class":351}," };\n",[323,1052,1054],{"class":325,"line":1053},18,[323,1055,937],{"class":351},[323,1057,1059],{"class":325,"line":1058},19,[323,1060,359],{"emptyLinePlaceholder":358},[323,1062,1064,1066,1068,1070,1072,1074,1076,1078,1081,1083,1085],{"class":325,"line":1063},20,[323,1065,795],{"class":329},[323,1067,798],{"class":384},[323,1069,801],{"class":369},[323,1071,433],{"class":351},[323,1073,897],{"class":333},[323,1075,900],{"class":373},[323,1077,341],{"class":340},[323,1079,1080],{"class":344},"challenge",[323,1082,348],{"class":340},[323,1084,818],{"class":384},[323,1086,446],{"class":351},[323,1088,1090,1092,1094,1096,1098,1100,1102,1104,1106],{"class":325,"line":1089},21,[323,1091,971],{"class":365},[323,1093,974],{"class":369},[323,1095,374],{"class":373},[323,1097,428],{"class":329},[323,1099,981],{"class":369},[323,1101,433],{"class":351},[323,1103,986],{"class":381},[323,1105,385],{"class":921},[323,1107,446],{"class":351},[323,1109,1111,1113,1115],{"class":325,"line":1110},22,[323,1112,560],{"class":559},[323,1114,563],{"class":333},[323,1116,464],{"class":351},[323,1118,1120,1122,1124,1126,1129,1131],{"class":325,"line":1119},23,[323,1121,1003],{"class":391},[323,1123,395],{"class":351},[323,1125,341],{"class":340},[323,1127,1128],{"class":344},"await_challenge_completion",[323,1130,348],{"class":340},[323,1132,464],{"class":351},[323,1134,1136,1139,1141,1143,1145,1148],{"class":325,"line":1135},24,[323,1137,1138],{"class":391},"    signup_evaluation_id",[323,1140,395],{"class":351},[323,1142,423],{"class":369},[323,1144,433],{"class":351},[323,1146,1147],{"class":333},"id",[323,1149,464],{"class":351},[323,1151,1153,1155,1157],{"class":325,"line":1152},25,[323,1154,589],{"class":351},[323,1156,408],{"class":921},[323,1158,352],{"class":351},[323,1160,1162,1164,1166,1169,1171,1173,1175,1178],{"class":325,"line":1161},26,[323,1163,916],{"class":329},[323,1165,1029],{"class":351},[323,1167,1168],{"class":391}," redirect",[323,1170,395],{"class":351},[323,1172,423],{"class":369},[323,1174,433],{"class":351},[323,1176,1177],{"class":333},"redirect",[323,1179,1050],{"class":351},[323,1181,1183],{"class":325,"line":1182},27,[323,1184,937],{"class":351},[208,1186,1187,1188,1190],{},"The ",[212,1189,1128],{}," user exists in your DB but can't authenticate yet. Two fields cover the binding:",[1192,1193,1194,1210],"table",{},[1195,1196,1197],"thead",{},[1198,1199,1200,1204,1207],"tr",{},[1201,1202,1203],"th",{},"Field",[1201,1205,1206],{},"Set when",[1201,1208,1209],{},"Reset when",[1211,1212,1213,1226],"tbody",{},[1198,1214,1215,1220,1223],{},[1216,1217,1218],"td",{},[212,1219,282],{},[1216,1221,1222],{},"On signup attempt",[1216,1224,1225],{},"Never!",[1198,1227,1228,1232,1240],{},[1216,1229,1230],{},[212,1231,286],{},[1216,1233,1234,1235,1237,1238],{},"On signup attempt: ",[212,1236,1010],{}," or ",[212,1239,1128],{},[1216,1241,1242,1243,1245],{},"Flipped to ",[212,1244,1010],{}," after challenge completes",[231,1247,1249],{"id":1248},"step-3-configure-the-challenge-success-url","Step 3: Configure the challenge success URL",[208,1251,1252,1253,1256,1257,1261,1262,433],{},"In the Rupt dashboard, on the relevant policy's Challenge Config (",[212,1254,1255],{},"Policies -> Edit -> Challenge Config","), set ",[1258,1259,1260],"strong",{},"Success URL"," to the page on your site that handles signup completion. For example: ",[212,1263,1264],{},"https:\u002F\u002Fyourapp.com\u002Fsignup\u002Fcomplete",[208,1266,1267],{},"When the user passes the challenge, Rupt redirects to that URL with the evaluation ID appended:",[315,1269,1274],{"className":1270,"code":1272,"language":1273},[1271],"language-text","https:\u002F\u002Fyourapp.com\u002Fsignup\u002Fcomplete?evaluation=68f…\n","text",[212,1275,1272],{"__ignoreMap":313},[208,1277,1278],{},"Configure it once per project. The same URL serves every signup that requires a challenge.",[231,1280,1282],{"id":1281},"step-4-complete-the-signup-server-side","Step 4: Complete the signup server-side",[208,1284,1285,1286,1289,1290,1293,1294,433],{},"Your ",[212,1287,1288],{},"\u002Fsignup\u002Fcomplete"," page (or route) takes the evaluation ID from the URL and POSTs it to your server. The server is the only thing that should flip the user's status. ",[1258,1291,1292],{},"Consume"," the evaluation in one shot — consuming is the read and the single-use claim at once, so there's no separate fetch. The first call returns the evaluation; a replay throws ",[212,1295,1296],{},"409",[315,1298,1300],{"className":747,"code":1299,"language":749,"meta":313,"style":313},"\u002F\u002F POST \u002Fsignup\u002Fcomplete\nconst { evaluation_id } = req.body;\n\n\u002F\u002F Cross-check against the user we created at \u002Fsignup\nconst user = await User.findOne({\n  signup_evaluation_id: evaluation_id,\n  signup_status: \"await_challenge_completion\",\n});\nif (!user) return reject(\"No pending signup matches this evaluation\");\n\n\u002F\u002F One shot: consume reads and claims the evaluation atomically. The first call\n\u002F\u002F wins; a replay throws 409.\nlet evaluation;\ntry {\n  evaluation = await rupt.consumeEvaluation(evaluation_id);\n} catch (err) {\n  if (err.status === 409)\n    return reject(\"This signup link was already used. Please log in.\");\n  \u002F\u002F Rupt unreachable: fail open. The status flip below still blocks replays.\n}\n\nif (evaluation && evaluation.challenge?.status !== \"completed\") {\n  return reject(\"Challenge not completed\");\n}\n\nuser.signup_status = \"complete\";\nawait user.save();\nreturn { token: issueToken(user), user };\n",[212,1301,1302,1307,1330,1334,1339,1360,1371,1386,1394,1424,1428,1433,1438,1446,1453,1477,1493,1515,1535,1540,1544,1548,1582,1601,1605,1609,1627,1643],{"__ignoreMap":313},[323,1303,1304],{"class":325,"line":326},[323,1305,1306],{"class":470},"\u002F\u002F POST \u002Fsignup\u002Fcomplete\n",[323,1308,1309,1311,1313,1316,1318,1320,1323,1325,1328],{"class":325,"line":355},[323,1310,366],{"class":365},[323,1312,1029],{"class":351},[323,1314,1315],{"class":369}," evaluation_id",[323,1317,405],{"class":351},[323,1319,374],{"class":373},[323,1321,1322],{"class":369}," req",[323,1324,433],{"class":351},[323,1326,1327],{"class":333},"body",[323,1329,352],{"class":351},[323,1331,1332],{"class":325,"line":362},[323,1333,359],{"emptyLinePlaceholder":358},[323,1335,1336],{"class":325,"line":413},[323,1337,1338],{"class":470},"\u002F\u002F Cross-check against the user we created at \u002Fsignup\n",[323,1340,1341,1343,1345,1347,1349,1351,1353,1356,1358],{"class":325,"line":418},[323,1342,366],{"class":365},[323,1344,974],{"class":369},[323,1346,374],{"class":373},[323,1348,428],{"class":329},[323,1350,981],{"class":369},[323,1352,433],{"class":351},[323,1354,1355],{"class":381},"findOne",[323,1357,385],{"class":384},[323,1359,446],{"class":351},[323,1361,1362,1365,1367,1369],{"class":325,"line":449},[323,1363,1364],{"class":391},"  signup_evaluation_id",[323,1366,395],{"class":351},[323,1368,1315],{"class":333},[323,1370,464],{"class":351},[323,1372,1373,1376,1378,1380,1382,1384],{"class":325,"line":467},[323,1374,1375],{"class":391},"  signup_status",[323,1377,395],{"class":351},[323,1379,341],{"class":340},[323,1381,1128],{"class":344},[323,1383,348],{"class":340},[323,1385,464],{"class":351},[323,1387,1388,1390,1392],{"class":325,"line":474},[323,1389,477],{"class":351},[323,1391,408],{"class":384},[323,1393,352],{"class":351},[323,1395,1396,1398,1400,1403,1405,1407,1409,1411,1413,1415,1418,1420,1422],{"class":325,"line":484},[323,1397,795],{"class":329},[323,1399,798],{"class":384},[323,1401,1402],{"class":373},"!",[323,1404,214],{"class":333},[323,1406,818],{"class":384},[323,1408,821],{"class":329},[323,1410,824],{"class":381},[323,1412,385],{"class":384},[323,1414,348],{"class":340},[323,1416,1417],{"class":344},"No pending signup matches this evaluation",[323,1419,348],{"class":340},[323,1421,408],{"class":384},[323,1423,352],{"class":351},[323,1425,1426],{"class":325,"line":489},[323,1427,359],{"emptyLinePlaceholder":358},[323,1429,1430],{"class":325,"line":495},[323,1431,1432],{"class":470},"\u002F\u002F One shot: consume reads and claims the evaluation atomically. The first call\n",[323,1434,1435],{"class":325,"line":519},[323,1436,1437],{"class":470},"\u002F\u002F wins; a replay throws 409.\n",[323,1439,1440,1442,1444],{"class":325,"line":536},[323,1441,617],{"class":365},[323,1443,423],{"class":333},[323,1445,352],{"class":351},[323,1447,1448,1451],{"class":325,"line":556},[323,1449,1450],{"class":329},"try",[323,1452,516],{"class":351},[323,1454,1455,1458,1460,1462,1464,1466,1469,1471,1473,1475],{"class":325,"line":568},[323,1456,1457],{"class":333},"  evaluation",[323,1459,374],{"class":373},[323,1461,428],{"class":329},[323,1463,370],{"class":369},[323,1465,433],{"class":351},[323,1467,1468],{"class":381},"consumeEvaluation",[323,1470,385],{"class":921},[323,1472,581],{"class":333},[323,1474,408],{"class":921},[323,1476,352],{"class":351},[323,1478,1479,1481,1484,1486,1489,1491],{"class":325,"line":586},[323,1480,477],{"class":351},[323,1482,1483],{"class":329}," catch",[323,1485,798],{"class":384},[323,1487,1488],{"class":333},"err",[323,1490,818],{"class":384},[323,1492,446],{"class":351},[323,1494,1495,1498,1500,1502,1504,1507,1509,1513],{"class":325,"line":596},[323,1496,1497],{"class":329},"  if",[323,1499,798],{"class":921},[323,1501,1488],{"class":369},[323,1503,433],{"class":351},[323,1505,1506],{"class":333},"status",[323,1508,900],{"class":373},[323,1510,1512],{"class":1511},"s4ofd"," 409",[323,1514,669],{"class":921},[323,1516,1517,1520,1522,1524,1526,1529,1531,1533],{"class":325,"line":1053},[323,1518,1519],{"class":329},"    return",[323,1521,824],{"class":381},[323,1523,385],{"class":921},[323,1525,348],{"class":340},[323,1527,1528],{"class":344},"This signup link was already used. Please log in.",[323,1530,348],{"class":340},[323,1532,408],{"class":921},[323,1534,352],{"class":351},[323,1536,1537],{"class":325,"line":1058},[323,1538,1539],{"class":470},"  \u002F\u002F Rupt unreachable: fail open. The status flip below still blocks replays.\n",[323,1541,1542],{"class":325,"line":1063},[323,1543,937],{"class":351},[323,1545,1546],{"class":325,"line":1089},[323,1547,359],{"emptyLinePlaceholder":358},[323,1549,1550,1552,1554,1556,1559,1561,1563,1565,1567,1569,1571,1573,1576,1578,1580],{"class":325,"line":1110},[323,1551,795],{"class":329},[323,1553,798],{"class":384},[323,1555,801],{"class":333},[323,1557,1558],{"class":373}," &&",[323,1560,423],{"class":369},[323,1562,433],{"class":351},[323,1564,1080],{"class":369},[323,1566,578],{"class":351},[323,1568,1506],{"class":333},[323,1570,809],{"class":373},[323,1572,341],{"class":340},[323,1574,1575],{"class":344},"completed",[323,1577,348],{"class":340},[323,1579,818],{"class":384},[323,1581,446],{"class":351},[323,1583,1584,1586,1588,1590,1592,1595,1597,1599],{"class":325,"line":1119},[323,1585,916],{"class":329},[323,1587,824],{"class":381},[323,1589,385],{"class":921},[323,1591,348],{"class":340},[323,1593,1594],{"class":344},"Challenge not completed",[323,1596,348],{"class":340},[323,1598,408],{"class":921},[323,1600,352],{"class":351},[323,1602,1603],{"class":325,"line":1135},[323,1604,937],{"class":351},[323,1606,1607],{"class":325,"line":1152},[323,1608,359],{"emptyLinePlaceholder":358},[323,1610,1611,1613,1615,1617,1619,1621,1623,1625],{"class":325,"line":1161},[323,1612,214],{"class":369},[323,1614,433],{"class":351},[323,1616,286],{"class":333},[323,1618,374],{"class":373},[323,1620,341],{"class":340},[323,1622,1010],{"class":344},[323,1624,348],{"class":340},[323,1626,352],{"class":351},[323,1628,1629,1631,1633,1635,1638,1641],{"class":325,"line":1182},[323,1630,498],{"class":329},[323,1632,974],{"class":369},[323,1634,433],{"class":351},[323,1636,1637],{"class":381},"save",[323,1639,1640],{"class":384},"()",[323,1642,352],{"class":351},[323,1644,1646,1648,1650,1652,1654,1656,1658,1660,1662,1664,1666],{"class":325,"line":1645},28,[323,1647,821],{"class":329},[323,1649,1029],{"class":351},[323,1651,1032],{"class":391},[323,1653,395],{"class":351},[323,1655,1037],{"class":381},[323,1657,385],{"class":384},[323,1659,214],{"class":333},[323,1661,408],{"class":384},[323,1663,513],{"class":351},[323,1665,974],{"class":333},[323,1667,1050],{"class":351},[208,1669,1670,1671,1673,1674,1677,1678,1680,1681,1683],{},"The lookup is by ",[212,1672,282],{},", not by trusting the ID in the query string. A user URL-tampering with someone else's ",[212,1675,1676],{},"evaluation="," won't find a pending row bound to themselves. And because the flip from ",[212,1679,1128],{}," to ",[212,1682,1010],{}," is itself single-use, consuming is a second gate: even if Rupt is down when you try to consume, a replay still finds no pending row.",[231,1685,1687],{"id":1686},"step-5-refuse-pending-users-at-login","Step 5: Refuse pending users at login",[208,1689,1690,1691,1693,1694,1696,1697,1699],{},"This is the bypass-prevention check. A user still in ",[212,1692,1128],{}," who skips ",[212,1695,1288],{}," and goes straight to ",[212,1698,247],{}," gets rejected.",[315,1701,1703],{"className":747,"code":1702,"language":749,"meta":313,"style":313},"\u002F** PSEUDO CODE **\u002F\n\u002F\u002F POST \u002Flogin\nconst user = await User.findOne({ email: form.email });\nif (!user) return reject(\"Invalid credentials\");\n\nif (user.signup_status === \"await_challenge_completion\") {\n  const evaluation = await rupt.getEvaluation(user.signup_evaluation_id);\n  return reject(\"Complete your signup challenge first.\", {\n    redirect: evaluation.redirect,\n  });\n}\n\u002F\u002F …password check, evaluate.login integrity check, etc.\n",[212,1704,1705,1710,1715,1752,1781,1785,1809,1837,1856,1871,1879,1883],{"__ignoreMap":313},[323,1706,1707],{"class":325,"line":326},[323,1708,1709],{"class":470},"\u002F** PSEUDO CODE **\u002F\n",[323,1711,1712],{"class":325,"line":355},[323,1713,1714],{"class":470},"\u002F\u002F POST \u002Flogin\n",[323,1716,1717,1719,1721,1723,1725,1727,1729,1731,1733,1735,1738,1740,1742,1744,1746,1748,1750],{"class":325,"line":362},[323,1718,366],{"class":365},[323,1720,974],{"class":369},[323,1722,374],{"class":373},[323,1724,428],{"class":329},[323,1726,981],{"class":369},[323,1728,433],{"class":351},[323,1730,1355],{"class":381},[323,1732,385],{"class":384},[323,1734,388],{"class":351},[323,1736,1737],{"class":391}," email",[323,1739,395],{"class":351},[323,1741,457],{"class":369},[323,1743,433],{"class":351},[323,1745,301],{"class":333},[323,1747,405],{"class":351},[323,1749,408],{"class":384},[323,1751,352],{"class":351},[323,1753,1754,1756,1758,1760,1762,1764,1766,1768,1770,1772,1775,1777,1779],{"class":325,"line":413},[323,1755,795],{"class":329},[323,1757,798],{"class":384},[323,1759,1402],{"class":373},[323,1761,214],{"class":333},[323,1763,818],{"class":384},[323,1765,821],{"class":329},[323,1767,824],{"class":381},[323,1769,385],{"class":384},[323,1771,348],{"class":340},[323,1773,1774],{"class":344},"Invalid credentials",[323,1776,348],{"class":340},[323,1778,408],{"class":384},[323,1780,352],{"class":351},[323,1782,1783],{"class":325,"line":418},[323,1784,359],{"emptyLinePlaceholder":358},[323,1786,1787,1789,1791,1793,1795,1797,1799,1801,1803,1805,1807],{"class":325,"line":449},[323,1788,795],{"class":329},[323,1790,798],{"class":384},[323,1792,214],{"class":369},[323,1794,433],{"class":351},[323,1796,286],{"class":333},[323,1798,900],{"class":373},[323,1800,341],{"class":340},[323,1802,1128],{"class":344},[323,1804,348],{"class":340},[323,1806,818],{"class":384},[323,1808,446],{"class":351},[323,1810,1811,1813,1815,1817,1819,1821,1823,1825,1827,1829,1831,1833,1835],{"class":325,"line":467},[323,1812,971],{"class":365},[323,1814,423],{"class":369},[323,1816,374],{"class":373},[323,1818,428],{"class":329},[323,1820,370],{"class":369},[323,1822,433],{"class":351},[323,1824,773],{"class":381},[323,1826,385],{"class":921},[323,1828,214],{"class":369},[323,1830,433],{"class":351},[323,1832,282],{"class":333},[323,1834,408],{"class":921},[323,1836,352],{"class":351},[323,1838,1839,1841,1843,1845,1847,1850,1852,1854],{"class":325,"line":474},[323,1840,916],{"class":329},[323,1842,824],{"class":381},[323,1844,385],{"class":921},[323,1846,348],{"class":340},[323,1848,1849],{"class":344},"Complete your signup challenge first.",[323,1851,348],{"class":340},[323,1853,513],{"class":351},[323,1855,516],{"class":351},[323,1857,1858,1861,1863,1865,1867,1869],{"class":325,"line":484},[323,1859,1860],{"class":391},"    redirect",[323,1862,395],{"class":351},[323,1864,423],{"class":369},[323,1866,433],{"class":351},[323,1868,1177],{"class":333},[323,1870,464],{"class":351},[323,1872,1873,1875,1877],{"class":325,"line":489},[323,1874,589],{"class":351},[323,1876,408],{"class":921},[323,1878,352],{"class":351},[323,1880,1881],{"class":325,"line":495},[323,1882,937],{"class":351},[323,1884,1885],{"class":325,"line":519},[323,1886,1887],{"class":470},"\u002F\u002F …password check, evaluate.login integrity check, etc.\n",[1889,1890,1891],"style",{},"html pre.shiki code .sAPXc, html code.shiki .sAPXc{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#C678DD;--shiki-default-font-style:inherit;--shiki-dark:#F92672;--shiki-dark-font-style:inherit}html pre.shiki code .seeE2, html code.shiki .seeE2{--shiki-light:#90A4AE;--shiki-default:#E06C75;--shiki-dark:#F8F8F2}html pre.shiki code .s9QZx, html code.shiki .s9QZx{--shiki-light:#39ADB5;--shiki-default:#98C379;--shiki-dark:#E6DB74}html pre.shiki code .siibJ, html code.shiki .siibJ{--shiki-light:#91B859;--shiki-default:#98C379;--shiki-dark:#E6DB74}html pre.shiki code .shEKG, html code.shiki .shEKG{--shiki-light:#39ADB5;--shiki-default:#ABB2BF;--shiki-dark:#F8F8F2}html pre.shiki code .sHm3x, html code.shiki .sHm3x{--shiki-light:#9C3EDA;--shiki-light-font-style:inherit;--shiki-default:#C678DD;--shiki-default-font-style:inherit;--shiki-dark:#66D9EF;--shiki-dark-font-style:italic}html pre.shiki code .sZ9uN, html code.shiki .sZ9uN{--shiki-light:#90A4AE;--shiki-default:#E5C07B;--shiki-dark:#F8F8F2}html pre.shiki code .sut_7, html code.shiki .sut_7{--shiki-light:#39ADB5;--shiki-default:#56B6C2;--shiki-dark:#F92672}html pre.shiki code .srTuz, html code.shiki .srTuz{--shiki-light:#39ADB5;--shiki-default:#C678DD;--shiki-dark:#F92672}html pre.shiki code .sjp9t, html code.shiki .sjp9t{--shiki-light:#6182B8;--shiki-default:#61AFEF;--shiki-dark:#A6E22E}html pre.shiki code .sJCYa, html code.shiki .sJCYa{--shiki-light:#90A4AE;--shiki-default:#ABB2BF;--shiki-dark:#F8F8F2}html pre.shiki code .sUwfj, html code.shiki .sUwfj{--shiki-light:#E53935;--shiki-default:#E06C75;--shiki-dark:#F8F8F2}html pre.shiki code .s42Qa, html code.shiki .s42Qa{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#7F848E;--shiki-default-font-style:italic;--shiki-dark:#88846F;--shiki-dark-font-style:inherit}html pre.shiki code .sKfv_, html code.shiki .sKfv_{--shiki-light:#39ADB5;--shiki-default:#ABB2BF;--shiki-dark:#F92672}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 .s2NTT, html code.shiki .s2NTT{--shiki-light:#F76D47;--shiki-default:#C678DD;--shiki-dark:#F92672}html pre.shiki code .sh6BQ, html code.shiki .sh6BQ{--shiki-light:#6182B8;--shiki-default:#61AFEF;--shiki-dark:#66D9EF}html pre.shiki code .s2Cpd, html code.shiki .s2Cpd{--shiki-light:#E53935;--shiki-default:#ABB2BF;--shiki-dark:#F8F8F2}html pre.shiki code .s4ofd, html code.shiki .s4ofd{--shiki-light:#F76D47;--shiki-default:#D19A66;--shiki-dark:#AE81FF}",{"title":313,"searchDepth":355,"depth":355,"links":1893},[1894,1895,1896,1897,1898,1899,1900],{"id":233,"depth":355,"text":234},{"id":271,"depth":355,"text":272},{"id":294,"depth":355,"text":295},{"id":740,"depth":355,"text":741},{"id":1248,"depth":355,"text":1249},{"id":1281,"depth":355,"text":1282},{"id":1686,"depth":355,"text":1687},"Signup is the awkward action. The user doesn't have an ID yet, so the usual evaluate flow doesn't quite fit — you'd normally pass user to Rupt and your server would later check that user.id on the evaluation matches. On the first signup there's nothing to match against, and a naive integration ends up letting attackers slip past the challenge by closing the tab and reloading your site.","md",{},"---\ntitle: Signup protection\n---\n\n# Signup protection\n\nSignup is the awkward action. The user doesn't have an ID yet, so the usual evaluate flow doesn't quite fit — you'd normally pass `user` to Rupt and your server would later check that `user.id` on the evaluation matches. On the first signup there's nothing to match against, and a naive integration ends up letting attackers slip past the challenge by closing the tab and reloading your site.\n\nIt builds on the generic [Challenge flow](\u002Fdocs\u002Fv3\u002Fchallenge-flow) and adds the binding that makes signup safe. Along with [Login protection](\u002Fdocs\u002Fv3\u002Ffundamentals\u002Flogin-protection), it's one of the two foundations every other guide builds on. Fake account, multi-accounting, and the other signup-time guides point back here because the shape is the same.\n\n## What this protects against\n\nSkip this pattern and you'll see these problems.\n\n- A user triggers a challenge, closes the tab, walks to `\u002Flogin`. Without server-side state they look like any other new account.\n- The client sends one email to Rupt and a different one to your `\u002Fregister`. Your server sees `verdict: allow` and trusts it.\n- Same email, several signup attempts, none challenge-completed, all of them landing in your DB.\n- Someone grabs the signup success URL after the challenge completed and replays it, hoping to mint a second session off the same evaluation.\n\nThe flow below closes them.\n\n## The flow\n\n\u003C!-- prettier-ignore-start -->\n::MermaidDiagram\n---\ncode: |\n  sequenceDiagram\n    actor U as User\n    participant C as Signup form\n    participant S as Your server\n    participant R as Rupt\n    participant X as Challenge UI\n\n    U->>C: Submits signup form (email, password)\n    C->>R: evaluate.signup({ email })\n    R-->>C: { evaluation_id, redirect? }\n    C->>S: POST \u002Fsignup { form fields, evaluation_id }\n    S->>R: GET \u002Fv3\u002Fevaluations\u002F{evaluation_id}\n    R-->>S: { verdict, user, challenge }\n    Note over S: Integrity check (action, email, verdict)\n\n    alt verdict = deny\n      S-->>C: Reject (401)\n      C-->>U: Show error\n    else verdict = allow\n      S->>S: Create user, signup_status = allowed\n      S-->>C: { token, user }\n      C-->>U: Logged in\n    else verdict = challenge\n      S->>S: Create user, signup_status = await_challenge_completion\n      S-->>C: { redirect }\n      C-->>U: Navigate to challenge URL\n      U->>X: Complete challenge\n      X->>S: Redirect to success_url?evaluation=…\n      S->>R: POST \u002Fv3\u002Fevaluations\u002F{evaluation_id}\u002Fconsume\n      R-->>S: { challenge.status, user } — or 409 if already used\n      Note over S: One shot — consume reads and claims at once,\u003Cbr\u002F>confirm it matches the pending user and the challenge completed\n      S->>S: Flip user.signup_status = complete\n      S-->>U: Issue token, navigate in\n    end\n---\n::\n\u003C!-- prettier-ignore-end -->\n\nYour server should keep a reference to the evaluation and the status of the signup on the User record. Something like `signup_evaluation_id` and `signup_status` on your `User` model do the trick. We'll fill in the details in the next steps.\n\n## Step 1: Call evaluate on the client\n\nOn signup you normally don't have a user ID yet. Pass `email` (and `phone` if you collect it).\n\n::ClientPlatform\n\n#web\n\n```js\nimport Rupt from \"@ruptjs\u002Fclient\";\n\nconst rupt = new Rupt({ clientId: \"your_client_id\" });\n\nconst evaluation = await rupt.evaluate.signup({\n  email: form.email,\n  \u002F\u002F phone: form.phone,\n});\n\n\u002F\u002F POST \u002Fsignup to your server with the evaluation ID\nawait fetch(\"YOUR_SIGNUP_ENDPOINT\u002Fsignup\", {\n  method: \"POST\",\n  body: JSON.stringify({\n    ...form,\n    evaluation_id: evaluation?.evaluation_id,\n  }),\n});\n```\n\n#ios\n\n```swift\nlet response = try await rupt.evaluate(\n  action: \"signup\",\n  email: form.email\n)\n\n\u002F\u002F POST evaluation.evaluationId to your server with the form\n```\n\n#android\n\n```kotlin\nval response = rupt.evaluate(\n  action = \"signup\",\n  email = form.email,\n)\n\n\u002F\u002F POST response.evaluationId to your server with the form\n```\n\n::\n\n## Step 2: Handle the verdict on your server\n\nYour server fetches the evaluation, runs the integrity check (the action and email match what you expected), and then handles the verdict.\n\n```ts\n\u002F\u002F POST \u002Fsignup\nconst evaluation = await rupt.getEvaluation(evaluation_id);\n\n\u002F\u002F Integrity check — block tampering before anything else\nif (evaluation.action !== \"signup\") return reject(\"Action mismatch\");\nif (evaluation.user?.email !== form.email) return reject(\"Identity mismatch\");\n\nif (evaluation.verdict === \"deny\") {\n  return reject(\"Signup denied\");\n}\n\nif (evaluation.verdict === \"allow\") {\n  const user = await User.create({\n    ...form,\n    signup_status: \"complete\",\n  });\n  return { token: issueToken(user), user };\n}\n\nif (evaluation.verdict === \"challenge\") {\n  const user = await User.create({\n    ...form,\n    signup_status: \"await_challenge_completion\",\n    signup_evaluation_id: evaluation.id,\n  });\n  return { redirect: evaluation.redirect };\n}\n```\n\nThe `await_challenge_completion` user exists in your DB but can't authenticate yet. Two fields cover the binding:\n\n| Field                  | Set when                                                      | Reset when                                      |\n| ---------------------- | ------------------------------------------------------------- | ----------------------------------------------- |\n| `signup_evaluation_id` | On signup attempt                                             | Never!                                          |\n| `signup_status`        | On signup attempt: `complete` or `await_challenge_completion` | Flipped to `complete` after challenge completes |\n\n## Step 3: Configure the challenge success URL\n\nIn the Rupt dashboard, on the relevant policy's Challenge Config (`Policies -> Edit -> Challenge Config`), set **Success URL** to the page on your site that handles signup completion. For example: `https:\u002F\u002Fyourapp.com\u002Fsignup\u002Fcomplete`.\n\nWhen the user passes the challenge, Rupt redirects to that URL with the evaluation ID appended:\n\n```\nhttps:\u002F\u002Fyourapp.com\u002Fsignup\u002Fcomplete?evaluation=68f…\n```\n\nConfigure it once per project. The same URL serves every signup that requires a challenge.\n\n## Step 4: Complete the signup server-side\n\nYour `\u002Fsignup\u002Fcomplete` page (or route) takes the evaluation ID from the URL and POSTs it to your server. The server is the only thing that should flip the user's status. **Consume** the evaluation in one shot — consuming is the read and the single-use claim at once, so there's no separate fetch. The first call returns the evaluation; a replay throws `409`.\n\n```ts\n\u002F\u002F POST \u002Fsignup\u002Fcomplete\nconst { evaluation_id } = req.body;\n\n\u002F\u002F Cross-check against the user we created at \u002Fsignup\nconst user = await User.findOne({\n  signup_evaluation_id: evaluation_id,\n  signup_status: \"await_challenge_completion\",\n});\nif (!user) return reject(\"No pending signup matches this evaluation\");\n\n\u002F\u002F One shot: consume reads and claims the evaluation atomically. The first call\n\u002F\u002F wins; a replay throws 409.\nlet evaluation;\ntry {\n  evaluation = await rupt.consumeEvaluation(evaluation_id);\n} catch (err) {\n  if (err.status === 409)\n    return reject(\"This signup link was already used. Please log in.\");\n  \u002F\u002F Rupt unreachable: fail open. The status flip below still blocks replays.\n}\n\nif (evaluation && evaluation.challenge?.status !== \"completed\") {\n  return reject(\"Challenge not completed\");\n}\n\nuser.signup_status = \"complete\";\nawait user.save();\nreturn { token: issueToken(user), user };\n```\n\nThe lookup is by `signup_evaluation_id`, not by trusting the ID in the query string. A user URL-tampering with someone else's `evaluation=` won't find a pending row bound to themselves. And because the flip from `await_challenge_completion` to `complete` is itself single-use, consuming is a second gate: even if Rupt is down when you try to consume, a replay still finds no pending row.\n\n## Step 5: Refuse pending users at login\n\nThis is the bypass-prevention check. A user still in `await_challenge_completion` who skips `\u002Fsignup\u002Fcomplete` and goes straight to `\u002Flogin` gets rejected.\n\n```ts\n\u002F** PSEUDO CODE **\u002F\n\u002F\u002F POST \u002Flogin\nconst user = await User.findOne({ email: form.email });\nif (!user) return reject(\"Invalid credentials\");\n\nif (user.signup_status === \"await_challenge_completion\") {\n  const evaluation = await rupt.getEvaluation(user.signup_evaluation_id);\n  return reject(\"Complete your signup challenge first.\", {\n    redirect: evaluation.redirect,\n  });\n}\n\u002F\u002F …password check, evaluate.login integrity check, etc.\n```\n",{"title":27,"description":1901},"Iw7BuoNojaALjUCPdCuWYtITJlW5xlV0a2xJv8pHLLg",1780344892813]