The fallback Dad ruled
다른 브라우져들처럼 자체 비밀번호 저장소를 만들어줘. 한번 기억한 id/pass 쌍은 다시 입력 안 해도 되게. 1password 연동 안 되면 이걸로 fallback.
Dad, 2026-10-04: "Make it a password store of its own, like other browsers have. Once it remembers an id and password pair, I shouldn't have to type it again. If the 1Password connection doesn't work, this is the fallback." It came the day after 1Password was parked. The word that matters is fallback: the daily friction had to go whether or not 1Password ever connects.
The index and the secret
The store splits every login in two. logins.json in Voyager's folder is the index: for each login an id, the origin, the username, a realm for HTTP authentication, and two dates, plus the sites Dad said never to ask about. The password is never there. It goes to the Keychain as one generic-password item per login, under the service com.cwk.voyager.logins, filed by the login's random UUID, readable only while the Mac is unlocked. A locked Keychain is its own error, so "cannot read it now" is never mistaken for "was never saved".
The vault is a protocol with two implementations: the Keychain in the installed app, and memory in the tests and the verification build, so a check leaves nothing behind in Dad's Keychain. The test theIndexOnDiskHoldsNoPassword reads the file back and looks. And the save writes in one order on purpose (first code block): the vault first, then the index. If the Keychain refuses, no row is written, so the index never lists a login it cannot fill.
Watching a sign-in, once
A small page script, loginWatcher, runs in the main frame of every page, in Voyager's own content world where the page's scripts cannot reach it. It tells the parent two things. Whether a login form is on the page, once per change. And, at the moment Dad submits one (a form submit, Enter in a password field, a click on a button), the name and the password he sent. For a sign-in in two steps it remembers the last name-like field he typed in, for ten minutes. A frame from another site inside the page is not the page's login, so only the main frame counts.
This is the one script in Voyager that reads a password field's value, and only at submission. Pippa's page-reading scripts name a password field by its label and never read what is in it; pippasReadingStillLeavesThePasswordOut holds that. The house sign adds a rule for whoever touches this code next: never log a login-submit message.
Offer and fill
After a submission the parent decides what to offer. Nothing, when the site is on the never-save list or the same name is already saved with the same password. "Update" for a known name with a new password. Otherwise "Save", with "Never for This Site" beside it. A private window offers nothing and saves nothing, and Settings can turn offering off.
Filling is the parent's decision too, never the page's. When a login form appears and exactly one login is saved for the site, and nothing was filled at this address yet, Voyager fills it. With several, a key appears in the address field and Dad chooses. A login belongs to a site when the scheme and port match and the host matches apart from a leading www., so a login saved over https is never offered to a plain-http page. HTTP authentication is remembered by realm and answered without the sheet, unless the server already refused it once. Settings → Passwords shows a password only after Touch ID or the Mac's password, the way Safari and Chrome ask.
Importing once, with the OS's consent
Dad's next question was whether the passwords Comet already held could come along. They can, once, through a consent chain where each link belongs to someone:
- Dad starts it, from Settings or the Bookmarks menu. A verification run refuses before anything is read.
- Voyager says what will happen before it happens (second code block).
- The key that protects the other browser's saved passwords is that browser's own Keychain item, so macOS, not Voyager, asks Dad whether Voyager may use it.
- Off the main thread, Voyager reads a copy of the profile's login databases once. Each password is decrypted in memory and goes only into the Keychain, through the same store.
- A login Voyager already has for that site and name stays as it is, a never-save site stays unsaved, and nothing of the other browser's is changed. The result is reported as counts: new, already here, never-save, not decrypted, not stored.
The importer opens two login databases alongside the bookmarks and history files (Login Data and Login Data For Account; the file's header comment still says three files in all), and never cookies, autofill or preferences. Nothing reaches the network. This lesson shows the consent's shape and leaves the cipher out. The tests take their vectors from Python's hashlib and openssl, never from this code, so the code is not graded against itself.
Passkeys, and the entitlement Dad declined
Measured on 2026-10-04: WebKit exposes PublicKeyCredential to a web view without Apple's web-browser entitlement, but isUserVerifyingPlatformAuthenticatorAvailable() answers false, so no page gets a Touch ID passkey. On iPhone and iPad that entitlement waits behind a public TestFlight link and external Beta App Review. Dad's answer:
아니, 근데 이거 다 디폴트 브라우져 자격 얻는데 필요한 깐깐한 심사 아니야? 디폴트로 안 쓰면 그만이잖아? 이걸 외부 베타 테스트 심사까지 받는다고? 그건 하면 안 돼.
"Isn't all this the strict review for becoming a default browser? If I don't use it as the default, that's that. Going through external beta review for this? We must not." The cost is written as current fact: cwkPippa's passkey sign-in shows no button in Voyager, and the family PIN signs in (Solo Auth Quest teaches that PIN). Track 9 tells the delivery side of the same decision.