{"release_id":"01a1998d-9802-0000-b0f7-33a23307f4ed","chunk_id":"01a1998d-9950-7dd2-8f8f-d788be1ff84e","file":"RFvBEy_r.mjs","mappings":"qTAoBA,MAAMA,EAAS,gBAIf,SAASC,GAAwB,CAChC,GAAI,CACH,OAAO,OAAO,eAAmB,IAAc,KAAO,cACvD,MAAQ,CAEP,OAAO,IACR,CACD,CAEO,SAASC,EAA8CC,EAAcC,EAAqB,CAChG,MAAMC,EAAIJ,EAAA,EACV,GAAI,CAACI,EAAG,MAAO,GACf,GAAI,CAKH,IAAIC,EAAK,KAAK,IAAA,EACd,MAAMC,EAAWF,EAAE,QAAQL,EAASG,CAAI,EACxC,GAAII,EACH,GAAI,CACH,MAAMC,EAAO,KAAK,MAAMD,CAAQ,EAC5B,OAAOC,EAAK,KAAQ,UAAY,KAAK,MAAQA,EAAK,KAAO,MAAQF,EAAKE,EAAK,IAChF,MAAQ,CAER,CAED,MAAMC,EAAoB,CAAE,GAAGL,EAAS,IAAKE,CAAA,EAC7C,OAAAD,EAAE,QAAQL,EAASG,EAAM,KAAK,UAAUM,CAAM,CAAC,EACxC,EACR,MAAQ,CACP,MAAO,EACR,CACD,CAIO,SAASC,EAAiBP,EAAwB,CACxD,MAAME,EAAIJ,EAAA,EACV,GAAI,CAACI,EAAG,OAAO,KACf,IAAIM,EAAqB,KACzB,GAAI,CACHA,EAAMN,EAAE,QAAQL,EAASG,CAAI,CAC9B,MAAQ,CACP,OAAO,IACR,CACA,GAAI,CAACQ,EAAK,OAAO,KACjB,GAAI,CACHN,EAAE,WAAWL,EAASG,CAAI,CAC3B,MAAQ,CAER,CACA,GAAI,CACH,MAAMS,EAAS,KAAK,MAAMD,CAAG,EAC7B,GAAI,OAAOC,EAAO,KAAQ,UAAY,KAAK,MAAQA,EAAO,IAAM,IAAQ,OAAO,KAE/E,KAAM,CAAE,IAAKC,EAAO,GAAGC,GAASF,EAEhC,OAAOE,CACR,MAAQ,CACP,OAAO,IACR,CACD,CAEO,SAASC,EAAYZ,EAAoB,CAC/C,MAAME,EAAIJ,EAAA,EACV,GAAKI,EACL,GAAI,CACHA,EAAE,WAAWL,EAASG,CAAI,CAC3B,MAAQ,CAER,CACD","names":["PREFIX","store","saveResume","flow","payload","s","ts","existing","prev","stored","consumeResume","raw","parsed","_drop","rest","clearResume"],"sources":["../../../../../../src/lib/oauth-resume.ts"],"sourcesContent":["// RAT-165: OAuth state-restore across the inline auth gates (rate, duel,\n// duel/[code], arena/submit). Google OAuth requires a full-page redirect to\n// the provider, which wipes the page's `$state`. We persist the in-flight\n// flow data (uploaded image path, selected mode, etc.) to localStorage\n// immediately before the redirect, then restore on the page's onMount once\n// the user lands back authenticated.\n//\n// sessionStorage, deliberately, not localStorage. The payload points at a photo\n// the user has already uploaded, and restore now runs on any logged-out return\n// to the page — not just the OAuth one. On localStorage that combination hands\n// the next person to pick up a shared phone or a library machine the previous\n// visitor's photo, ready to submit under their own name. sessionStorage is\n// scoped to the tab: it survives the full-page redirect out to Google and back\n// (same browsing context, so every legitimate resume still works) and dies with\n// the tab, so it can never reach a second person.\n//\n// Entries are keyed per flow and carry a 10-minute TTL measured from the FIRST\n// save, so a stale resume can't hijack a fresh upload.\n\nconst TTL_MS = 10 * 60 * 1000;\nconst PREFIX = 'oauth_resume_';\n\ntype Stored<T> = T & { _ts: number };\n\nfunction store(): Storage | null {\n\ttry {\n\t\treturn typeof sessionStorage === 'undefined' ? null : sessionStorage;\n\t} catch {\n\t\t// Safari private mode / storage blocked.\n\t\treturn null;\n\t}\n}\n\nexport function saveResume<T extends Record<string, unknown>>(flow: string, payload: T): boolean {\n\tconst s = store();\n\tif (!s) return false;\n\ttry {\n\t\t// Keep the original timestamp when re-saving. saveResume runs on every step\n\t\t// transition, and re-stamping it there turned the 10-minute cap into \"10\n\t\t// minutes since the last click\" — extendable forever, which is exactly the\n\t\t// stale-hijack window the TTL exists to close.\n\t\tlet ts = Date.now();\n\t\tconst existing = s.getItem(PREFIX + flow);\n\t\tif (existing) {\n\t\t\ttry {\n\t\t\t\tconst prev = JSON.parse(existing) as { _ts?: unknown };\n\t\t\t\tif (typeof prev._ts === 'number' && Date.now() - prev._ts <= TTL_MS) ts = prev._ts;\n\t\t\t} catch {\n\t\t\t\t// unparseable — treat as a fresh save\n\t\t\t}\n\t\t}\n\t\tconst stored: Stored<T> = { ...payload, _ts: ts };\n\t\ts.setItem(PREFIX + flow, JSON.stringify(stored));\n\t\treturn true;\n\t} catch {\n\t\treturn false;\n\t}\n}\n\n/** Reads and consumes (deletes) the resume payload. Stale entries (>TTL) are\n *  also cleared and return null. */\nexport function consumeResume<T>(flow: string): T | null {\n\tconst s = store();\n\tif (!s) return null;\n\tlet raw: string | null = null;\n\ttry {\n\t\traw = s.getItem(PREFIX + flow);\n\t} catch {\n\t\treturn null;\n\t}\n\tif (!raw) return null;\n\ttry {\n\t\ts.removeItem(PREFIX + flow);\n\t} catch {\n\t\t// best effort\n\t}\n\ttry {\n\t\tconst parsed = JSON.parse(raw) as Stored<T>;\n\t\tif (typeof parsed._ts !== 'number' || Date.now() - parsed._ts > TTL_MS) return null;\n\t\t// strip _ts before handing back\n\t\tconst { _ts: _drop, ...rest } = parsed;\n\t\tvoid _drop;\n\t\treturn rest as unknown as T;\n\t} catch {\n\t\treturn null;\n\t}\n}\n\nexport function clearResume(flow: string): void {\n\tconst s = store();\n\tif (!s) return;\n\ttry {\n\t\ts.removeItem(PREFIX + flow);\n\t} catch {\n\t\t// best effort\n\t}\n}\n\n/** Synchronous \"is there a fresh resume entry?\" check used at component init\n *  to suppress the upload-screen flash on the OAuth-return path. Doesn't\n *  consume the entry. SSR-safe: returns false when localStorage isn't\n *  available. Uses the same TTL as consumeResume so a stale entry doesn't\n *  trigger a misleading loading screen. */\nexport function hasResume(flow: string): boolean {\n\tconst s = store();\n\tif (!s) return false;\n\ttry {\n\t\tconst raw = s.getItem(PREFIX + flow);\n\t\tif (!raw) return false;\n\t\tconst parsed = JSON.parse(raw) as { _ts?: unknown };\n\t\tif (typeof parsed._ts !== 'number' || Date.now() - parsed._ts > TTL_MS) return false;\n\t\treturn true;\n\t} catch {\n\t\treturn false;\n\t}\n}\n"],"version":3}