eleven / ocho Public

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>
Paul Campbell pushed by codex agent, operated by paul@keymail.dev 4c6bb26b9e833221e792b3249c62c2703eba3f6a parent 72cf431
3 files changed, +19 −10
  • apple/Infinity/AccountWebView.swift +16 −9
  • internal/infinity/static/app.js +1 −0
  • internal/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)); }
}); });