apple: remember the account after authenticated client startup
Navigation completion precedes async setup and restore. Persist only the origin from the client-ready bridge message, validate its frame origin, clear recovery passwords after export, and describe download state honestly. Chromium, WebKit, macOS and iOS gates pass.
Co-Authored-By: Codex <noreply@openai.com>
3 files changed,
+19
−10
apple/Infinity/AccountWebView.swift+16 −9internal/infinity/static/app.js+1 −0internal/infinity/static/dialogs.js+2 −1
diff --git a/apple/Infinity/AccountWebView.swift b/apple/Infinity/AccountWebView.swift| index 2746d50..ea9c4c8 100644 |
| --- a/apple/Infinity/AccountWebView.swift |
| +++ b/apple/Infinity/AccountWebView.swift |
| @@ -44,14 +44,6 @@ struct AccountWebView { |
| var lastURL: URL? |
| var reloadID = 0 |
| init(location: AccountLocation) { self.location = location } |
| - func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { |
| - // Remember only a confirmed owned account origin, never a setup/recovery fragment. |
| - webView.evaluateJavaScript("(() => { const id = JSON.parse(localStorage.getItem('infinity.identity.v1') || 'null'); return id && id.origin === location.origin ? location.origin : null; })()") { result, _ in |
| - if let origin = result as? String, let url = URL(string: origin), url.scheme == "https" { |
| - UserDefaults.standard.set(origin, forKey: "infinity.account-origin") |
| - } |
| - } |
| - } |
| func webView(_ webView: WKWebView, decidePolicyFor action: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { |
| guard let url = action.request.url else { decisionHandler(.cancel); return } |
| if url.scheme == "https" || url.scheme == "about" || url.scheme == "blob" { decisionHandler(.allow); return } |
| @@ -63,7 +55,22 @@ struct AccountWebView { |
| } |
| func userContentController(_ controller: WKUserContentController, didReceive message: WKScriptMessage) { |
| guard message.frameInfo.isMainFrame, |
| - let body = message.body as? [String: Any], body["type"] as? String == "recovery", |
| + message.frameInfo.securityOrigin.protocol == "https", |
| + let body = message.body as? [String: Any] else { return } |
| + if body["type"] as? String == "account" { |
| + // The client sends this after authenticated setup/restore finishes; |
| + // didFinish navigation fires before that asynchronous work completes. |
| + guard let origin = body["origin"] as? String, |
| + let url = URL(string: origin), url.scheme == "https", |
| + url.host == message.frameInfo.securityOrigin.host, |
| + (url.port ?? 443) == (message.frameInfo.securityOrigin.port == 0 ? 443 : message.frameInfo.securityOrigin.port), |
| + url.user == nil, url.password == nil, |
| + url.path.isEmpty || url.path == "/", |
| + url.query == nil, url.fragment == nil else { return } |
| + UserDefaults.standard.set(origin, forKey: "infinity.account-origin") |
| + return |
| + } |
| + guard body["type"] as? String == "recovery", |
| let text = body["content"] as? String, text.utf8.count < 100_000, |
| let data = text.data(using: .utf8), |
| let object = try? JSONSerialization.jsonObject(with: data) as? [String: Any], |
diff --git a/internal/infinity/static/app.js b/internal/infinity/static/app.js| index 719e6a2..735fab7 100644 |
| --- a/internal/infinity/static/app.js |
| +++ b/internal/infinity/static/app.js |
| @@ -8,6 +8,7 @@ const root = document.querySelector('#app'); |
| async function ready(client) { |
| const params = fragment(); history.replaceState(null, '', '/'); |
| const chat = new Chat(root, client); await chat.start(params.invite); |
| + window.webkit?.messageHandlers?.infinity?.postMessage({ type: 'account', origin: client.id.origin }); |
| } |
| async function boot() { |
| const params = fragment(); |
diff --git a/internal/infinity/static/dialogs.js b/internal/infinity/static/dialogs.js| index 78a2673..c73958b 100644 |
| --- a/internal/infinity/static/dialogs.js |
| +++ b/internal/infinity/static/dialogs.js |
| @@ -38,11 +38,12 @@ export function recovery(client) { |
| const file = await exportRecovery(client.id, password.input.value); |
| if (window.webkit?.messageHandlers?.infinity) { |
| window.webkit.messageHandlers.infinity.postMessage({ type: 'recovery', content: file }); |
| + password.input.value = ''; confirm.input.value = ''; |
| note.replaceChildren(message('Choose where to save your recovery file.')); return; |
| } |
| const url = URL.createObjectURL(new Blob([file], { type: 'application/json' })); |
| const a = el('a', { href: url, download: 'infinity-recovery.json' }); document.body.append(a); a.click(); a.remove(); setTimeout(() => URL.revokeObjectURL(url), 60000); |
| - note.replaceChildren(message('Recovery file saved. On another device, choose “I have a recovery file”.')); |
| + note.replaceChildren(message('Save the downloaded file. On another device, choose “I have a recovery file”.')); |
| password.input.value = ''; confirm.input.value = ''; |
| } catch (error) { note.replaceChildren(message(error.message, true)); } |
| }); }); |