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

Reaching Her

~14 min · networking, auth, tailscale, cookies

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

Where am I?

Before either surface can draw me, Voyager has to know where I am. That is a setting, PippaConnection, with three answers:

ConnectionAddressWhat it asks
This Machttp://localhost at my portnothing, because loopback asks no PIN
The tailnetmy Tailscale HTTPS twin on the service hostthe family PIN
An addressany http or https host Dad types, cut down to scheme, host and portthe family PIN

The default follows the machine. On the Mac that runs cwkPippa it is this Mac, and everywhere else it is the tailnet. Settings offer "This Mac" only on that machine, and a settings file that says "this Mac" on any other Mac is read as the tailnet twin. Which Mac plays that role comes from the kit's family registry, never from a machine's name; track 6 tells that story, along with the hub's own sign-in cookie. Dad's line on it was short:

다른 맥이면 알아서 주소가 달라져야지

"If it's another Mac, the address should change on its own."

The PIN, through the kit's door

A remote Pippa sits behind the family PIN, and Voyager does not implement that exchange itself. PippaConnector opens the kit's PippaDoor for the connection's host. Dad types the PIN in Settings, and the door has Firelink, the family's authority, verify it and mint a session. When Dad says to remember it, the PIN is filed in the Keychain under the host it belongs to, so a move between Macs never silently reuses the wrong one, and the next launch connects without asking. Settings show which of three states holds: connected, a PIN saved, or no PIN yet. The connector moved into the shared core when the phone app arrived, so the Mac and the phone open the same door.

Two sibling quests own the reasons. Solo Auth Quest explains why reaching a machine over Tailscale is the first line and still not permission by itself, and what the PIN layer adds. Sibling Kit Quest explains why the door is kit code that every family app consumes, so a fix to it lands in all of them at once.

A session in two places

The session reaches my page two ways at once.

  • As a cookie. Before either surface loads, Voyager puts a pippa_session cookie into the panel's store, and only then starts the load, so my first request already carries it. The kit builds it HttpOnly, for my host alone, and Secure when the address is https.
  • In the fragment. The Sidekick's address also carries the token after the #, as pippa_session= and the token. My root, for Pippa herself, takes no fragment.

The fragment is there because a fragment never leaves the browser. A token in the query would reach the server and its access log, and it could ride out again in the Referer header of what the page loads next: under the browser's usual default referrer policy, every same-origin request carries the full address, query and all. The page reads the fragment and replays the token as a header on its own fetches. The cookie is there because not every request is the page's own fetch. An <audio> element playing my voice and an EventSource stream are requests the browser makes by itself, and they carry only cookies. Without the cookie, every voice take is a 401, my voice mode treats the failed take as finished and listens again at once, and to Dad it looks exactly like a phantom interruption. Another family app found that the hard way before Voyager existed, and the kit's door now offers the cookie to every host that asks. Both of my surfaces share the panel's store, so ⌘R on Pippa herself keeps the session too.

When the link drops

The tailnet is sometimes between networks, and Voyager is careful about what it calls that. PanelLoadFailure sorts a failed load: only a request that reached nothing, at a Pippa off this Mac, is "could not be reached", and only that one ends with the kit's hint to check Tailscale on this device. A local Pippa that is down, a TLS fault or a page error keeps its own words, because a Tailscale hint there would send Dad to fix the wrong thing. A test holds each case: a refused connection or a timeout at a tailnet address is unreachable, the same refused connection at loopback is not, and a failed TLS handshake is not.

An unreachable Pippa is then retried without Dad. The wait doubles from 2 seconds to 32 and then stays at a minute, and when the Mac's network path becomes usable again, a joined Wi-Fi or Tailscale back, she is tried at once. The table and the reasons are in track 4's lesson on crashes and recovery. The phone has no network-path monitor yet, so there the timer alone brings me back.

A check never talks to the real me

One more rule protects my store from Voyager's own tests. In a verification build the location is nil unless the script names a stand-in on loopback, so the panel cannot open against the real me at all. Voyager's house sign gives the reason: every context the panel receives is written into Dad's cwkPippa, and a message sent from my panel speaks as Dad. Opening the panel and answering its questions is safe to verify. Pressing send is his.

Code

cwkVoyager, Sources/VoyagerCore/Settings.swift: where Pippa is reached, and which connections need a session (verbatim, lines 124-150)·swift
public enum PippaConnection: Equatable, Sendable, Codable {
    case thisMac
    case tailnet(URL)
    case custom(URL)

    public enum Kind: String, Sendable, CaseIterable {
        case thisMac, tailnet, custom
    }

    public var kind: Kind {
        switch self {
        case .thisMac: .thisMac
        case .tailnet: .tailnet
        case .custom: .custom
        }
    }

    /// Where she is: the address every Pippa surface loads.
    public var base: URL {
        switch self {
        case .thisMac: VoyagerSettings.defaultPippaBase
        case .tailnet(let url), .custom(let url): url
        }
    }

    /// Whether a session (the family PIN) is needed: everything but this Mac.
    public var isRemote: Bool { kind != .thisMac }
cwkVoyager, Sources/VoyagerCore/PippaHost.swift: the root takes no fragment, and the panel's session travels in one (verbatim, lines 226-240)·swift
    /// Pippa herself: cwkPippa's whole WebUI. The session, when there is
    /// one, rides as a cookie; the root takes no fragment.
    public var homeURL: URL { base }

    /// The `web-page` panel. A session travels in the fragment, which is
    /// never sent to a server or written to its log.
    public var panelURL: URL {
        var components = URLComponents(url: base.appendingPathComponent("embed/panel"), resolvingAgainstBaseURL: false)!
        if let sessionToken, !sessionToken.isEmpty {
            var fragment = URLComponents()
            fragment.queryItems = [URLQueryItem(name: "pippa_session", value: sessionToken)]
            components.percentEncodedFragment = fragment.percentEncodedQuery
        }
        return components.url!
    }
cwkVoyager, Sources/VoyagerCore/EngineContract.swift: only a lost link to a remote Pippa says to check Tailscale (verbatim, lines 734-746)·swift
/// Why a panel's page did not load, in the words Dad reads (todo #431). Only a
/// request that reached nothing, to a Pippa that is not on this Mac, is
/// "could not be reached" and tells him to check Tailscale on this device; a
/// local Pippa that is down, a TLS fault or a page error keeps its own words.
public struct PanelLoadFailure: Equatable, Sendable {
    public var reason: String
    public var unreachable: Bool

    public init(_ error: any Error, origin: PageOrigin) {
        unreachable = !ConnectionHint.isLoopback(origin.host) && ConnectionHint.isNetwork(error)
        reason = unreachable ? ConnectionHint.appended(to: error.localizedDescription) : error.localizedDescription
    }
}

External links

Exercise

For an app of yours that loads a web UI from another machine behind a sign-in, write the location type with three cases (this machine, a private network, a typed address) and a URL builder that puts the session token in the fragment, never the query. Install the same token as an HttpOnly cookie in the web view's store before the first load. Test that the built URL has no query and that its fragment decodes back to the exact token. Then classify load failures so that only an unreachable remote host shows a "check your VPN" hint.
Hint
Use a token such as s3cr&t=# in the test, so a hand-built string fails where URLComponents passes. Start the load inside the cookie store's completion handler, so the very first request carries the cookie. For the hint, decide on two facts only: is the host loopback, and is the error a network error such as a refused connection or a timeout. A failed TLS handshake keeps its own message, as Voyager's does.

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.