Skip to content
C.W.K.
Lesson 04 of 05 · published

1Password, Honestly

~14 min · debugging, 1password, case-study, stopping-conditions

Level 0New Tab
0 XP0/44 lessons0/13 achievements
0/100 XP to next level100 XP to go0% complete

Two programs that must trust each other

In a Chromium browser, 1Password is two programs: the extension inside the browser and the desktop app beside it. They meet over native messaging, and the desktop app's helper verifies the browser before it answers anything. For a browser it does not already know, the desktop app offers Add browser. Dad uses 1Password, so it was the first case on Voyager's engine gate, W1, before a single brick was laid.

This lesson is the browser's side of that story, told as a debugging case study. It ends where the work really ended, which is not at a fix.

What passed, wall by wall

WallWhat stood in the wayWhat removed it
Missing Chrome namesThe background stopped at the first name WebKit leaves outThe compatibility layer (lesson 2)
The signatureThe helper could not read the family-signed build's signature; 1Password's core checks for a hardened-runtime signature with bundle and team identifiers under Apple's chainThe installed bundle signed with the paid team's Apple Development identity and the hardened runtime. Dad added Voyager through Add browser, and 1Password lists it as a connected browser. Track 2's gate lesson tells that result; track 9 teaches the signing
The process groupEvery host the bridge started was refused once a minuteposix_spawn in the browser's own group (lesson 3)
The direct parentA relay in between was refused by a second ruleNo intermediate: the app itself is the parent (lesson 3)
The extension pageThe chrome-extension:// address was dropped, its scheme cancelled, and the page refused without the context's own configurationpageURL(for:) and a page built with that configuration (lesson 3)
The fragmentThe extension's app page opened as a dark tabThe fragment preserved, with a regression test (below)

After the fourth row the helper logged that it was verifying Voyager and waiting for partner authorization. That wait is for a message the extension's own background sends when its app page asks it to, which is why the last two rows mattered.

The dark page and the fragment

The app page first showed up as a dark tab titled "1Password". An early note read that as the extension's front-end app failing to mount, and named a framework for it. A later inspection of the installed package corrected both claims. The app's entry point is React with a hash router: it has no route for /, and its welcome route is #/page/welcome. Voyager's pageURL(for:) had been building the page URL without the fragment, so the router had nowhere to go. The probe used to check it had opened the bare app/app.html too, so the instrument shared the bug it was looking for.

The mapping now carries the fragment over with its encoding intact (first code block), and anExtensionPageKeepsItsFragmentAndRendersItsHashRoute holds it: absent, empty and encoded fragments, a route's own query inside the fragment, and a fixture page that renders only when its hash route arrives (second code block). The house sign keeps the lesson in one sentence: "A dark page alone does not establish that its JavaScript failed to mount."

The dead toolbar button got its explanation the same way. Unpaired, 1Password's background clears its own pop-up a few seconds after it loads, so a click belongs to the background's click event, which shows nothing until pairing completes. The dead button was the unpaired state showing through.

What did not pass

Authentication through the extension did not hold. The pop-up and filling a login were never seen working in Voyager. And while the extension stays unpaired it opens a fresh native port every minute, so waiting helpers pile up on the Mac.

One bounded diagnosis followed, with its limits written before it ran: a hidden probe on public API only, fresh isolated controller and data stores, no desktop bridge, no real tabs, no images captured, and no credentials, vaults or protocol bodies read. It ruled one hypothesis out. WebKit gives each extension context a generated origin that changed between processes, yet data stored under it survived a second process, so a changing origin alone does not explain lost sign-in state on that Mac. The public package also rendered its welcome route in an empty profile. Neither finding is a cause. The record says it in its own words: "The actual root cause and a precise corrective patch are not established."

Stopping is part of the diagnosis

Dad's stopping condition ended it there. The attempt is finished, 1Password is parked last with no further hypothesis-and-repair cycles, and Home, accounts, Incognito and Pippa go ahead without it. The record also fences the next attempt: callback, action-state and completion metadata only, or a check Dad assists, and no new install, restart, trust grant or credential use inferred from anything written there. No Voyager commit since has touched 1Password.

So two documents say two true things. The plan and the gate say 1Password lists Voyager as a connected browser. The diagnosis says the integration is unresolved. They describe different halves. One decision from this work stayed: on cwkPippa's own pages, 1Password's content script took over navigator.credentials, and a passkey could not be saved while its pairing was open. That case is why every extension is now kept off the family's hosts (track 6). The next day Dad ruled in the fallback, and the last lesson is that.

Code

cwkVoyager, Sources/VoyagerWebKit/WebKitExtensionHost.swift: a chrome-extension:// address mapped to the installed page, fragment kept verbatim (verbatim, lines 347-367)·swift
    /// A `chrome-extension://` address from outside — a desktop app pairing
    /// its extension opens the extension's own page by this address
    /// (1Password's Add browser), the way Chrome does — becomes the
    /// installed package's page URL in WebKit's scheme. Nil when the
    /// address belongs to no installed extension.
    public func pageURL(for url: URL) -> URL? {
        guard url.scheme == "chrome-extension",
              let id = url.host,
              let context = contexts[id]
        else { return nil }
        let base = context.baseURL
        let components = URLComponents(url: url, resolvingAgainstBaseURL: false)
        let path = components?.path ?? ""
        let page = path.isEmpty ? base : base.appendingPathComponent(String(path.dropFirst()))
        guard var pageComponents = URLComponents(url: page, resolvingAgainstBaseURL: false) else { return nil }
        pageComponents.query = components?.query
        // Extension apps route inside the fragment (for example 1Password's
        // #/page/welcome). Preserve its encoding and its own query verbatim.
        pageComponents.percentEncodedFragment = components?.percentEncodedFragment
        return pageComponents.url
    }
cwkVoyager, Tests/VoyagerWebKitTests/ExtensionHostTests.swift: the regression test for absent, empty and encoded fragments and a rendered hash route (verbatim, lines 193-209)·swift
        let base = "chrome-extension://\(Self.extensionID)/route.html?from=app"
        // Preserve absent, empty and encoded fragments, including a route's own query.
        for suffix in ["", "#", "#/page/welcome?language=en", "#/page/welcome?label=a%20b%23c%26d"] {
            let input = try #require(URL(string: base + suffix))
            let mapped = try #require(host.pageURL(for: input))
            let original = try #require(URLComponents(url: input, resolvingAgainstBaseURL: false))
            let result = try #require(URLComponents(url: mapped, resolvingAgainstBaseURL: false))
            #expect(result.scheme == "webkit-extension")
            #expect(result.path == "/route.html")
            #expect(result.query == "from=app")
            #expect(result.percentEncodedFragment == original.percentEncodedFragment)
        }
        let input = try #require(URL(string: base + "#/page/welcome?language=en"))
        let mapped = try #require(host.pageURL(for: input))
        let page = try #require(engine.makePage(forExtensionPageAt: mapped))
        page.load(mapped)
        #expect(await eventually { page.status.title == "Fixture welcome" })

External links

Exercise

Pick an integration in your own software that is stuck: a plugin that loads but does nothing, a sign-in that never completes. Write its record the way this lesson's table works: each wall, the evidence that it stood, and the evidence that it fell. Then write the stopping condition before your next attempt: what you will try, what you will not read or touch, and what result ends the attempt either way.
Hint
Keep "passed" and "works" in separate columns; a verified handshake is not a working feature. For each piece of evidence, note what the instrument could not see. A good stopping condition names a budget (attempts or hours) and a decision owner, not only a goal.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign in — Please sign in to comment.

No comments yet — be the first.