RSSAmplifier

Blog

TermOnMac — Engineering Notes

Technical notes on building TermOnMac: cryptography, PTY sessions, Cloudflare Durable Objects, and the rest of a zero-knowledge Mac terminal relay.

termonmac.comRSS feed ↗33 posts

Latest posts

Using Durable Objects as a WebSocket Session State Machine

How TermOnMac's Cloudflare Durable Object manages Mac and iPhone WebSocket slots, handles quota checks at connection time, and survives hibernation via tagged socket recovery.

Surviving Cloudflare DO Hibernation Without Dropping WebSocket Connections

Cloudflare Durable Objects hibernate when idle, losing in-memory state. How TermOnMac rebuilds WebSocket role mappings from storage-backed UUID tags on every wake.

Per-User Strong Consistency: Subscription State in a Durable Object

Why TermOnMac's subscription tier lives in a per-user Durable Object instead of KV — serialized writes, lazy expiry, notification dedup, and stale downgrade protection.

Building a Credit-Based Quota System with 5-Hour Rolling Windows

TermOnMac charges 1 credit per relay message and 2 per minute of DO runtime. How the 5-hour window, welcome bonus, and split KV keys protect admin grants from stale writes.

Enforcing Per-User Room Limits Without a Database

TermOnMac caps concurrent Mac rooms per tier using a Cloudflare KV prefix scan with 15-minute TTLs — no counters, no cleanup job, reconnects always allowed.

Forward Secrecy in a Reconnectable Protocol: Two Key Pairs Per Session

Why TermOnMac uses a persistent identity key for TOFU and a per-connection ephemeral X25519 key for ECDH — so a stolen identity key cannot decrypt past recorded traffic.

forkpty() on macOS: The Login Shell Setup

How TermOnMac spawns a login shell via forkpty() on macOS — argv[0] tricks, controlling TTY setup, and why SHELL_SESSIONS_DISABLE matters for a programmatic PTY.

SHELL_SESSIONS_DISABLE and Other macOS Shell Environment Setup

The four environment variables TermOnMac sets before execvp — TERM, LANG, SHELL_SESSIONS_DISABLE, TERMONMAC_SESSION — and why each matters for a programmatic PTY.

Non-Blocking PTY I/O with GCD Dispatch Sources

How TermOnMac reads from the PTY master FD without blocking a thread — O_NONBLOCK plus a DispatchSourceRead on the userInteractive queue, with EAGAIN-aware writes.

PTY Output Buffering: 32KB or 200ms, Whichever Comes First

Why TermOnMac doesn't send PTY output on every read — a dual-trigger buffer plus a local terminal query interceptor that answers vim's cursor queries without a round trip.

RingBuffer for Terminal Replay: Restoring State After Reconnect

Each TermOnMac PTY session keeps a per-session RingBuffer so the iPhone can replay the current terminal state after a network drop, with incremental offset-based delta delivery.

Integrating SwiftTerm into a SwiftUI App

How TermOnMac wraps SwiftTerm's CustomTerminalView in a UIViewRepresentable for SwiftUI — feeding PTY bytes, handling resets on reconnect, and blocking accidental pastes.

PTY Resize: Buffer Switch and Reinit Triggers in SwiftTerm

How TermOnMac propagates iPhone terminal size changes to the Mac via TIOCSWINSZ — buffer switches, reinit callbacks, Ctrl-L prompt redraws, and debounced resize events.

Building a Terminal Keyboard Toolbar on iOS

iOS keyboards don't have Ctrl, Esc, or arrow keys. How TermOnMac builds a custom inputAccessoryView with debounced shortcut search, modifier toggles, and focus handling.

Preserving Scroll Position While Terminal Output Keeps Arriving

How TermOnMac keeps your reading position when you're scrolled up and new output arrives — saved scroll ratios, reset-before-replay, and local scroll shortcuts.

Reconnecting Without Hammering the Server: Exponential Backoff with NetworkMonitor

How TermOnMac's iOS app handles lost WebSocket connections — exponential backoff, NWPathMonitor-driven immediate reconnect, input buffering, and a 5-minute total timeout.

Heartbeat Design: Keeping Durable Objects Awake

Why TermOnMac's Mac heartbeat flips between 30s and 1s during active use — Cloudflare DO hibernation, not dead-connection detection, is the real driver behind the cadence.

Batching WebSocket Messages: How relay_batch Reduces Overhead

Why TermOnMac coalesces up to 20 encrypted payloads per WebSocket message — not for framing overhead, but because the relay charges 1 credit per batch instead of per payload.

The attach/detach Model: PTY Keeps Running While You're Away

How TermOnMac separates PTY session lifetime from iOS attachment — two session lists, buffer-only mode, draft input preservation, and full-remove destruction via ptyDestroy.

Session Takeover: When a Second Device Connects to the Same PTY

How TermOnMac handles two iOS devices racing to attach to the same PTY — an isTakenOver flag, explicit takeover callbacks, and relay-enforced single-iOS-socket per room.

Unix Domain Socket IPC: How the Mac CLI Talks to the Helper Daemon

How TermOnMac's short-lived CLI talks to the long-running helper daemon — a chmod 0600 Unix socket with a length-prefixed JSON protocol and explicit IPC versioning.

Multi-Provider OAuth with Account Linking: GitHub, Google, and Apple Sign In

How TermOnMac links GitHub, Google, and Apple logins to a single user by email — KV key layout, account linking, pending-delete reversal, and email index backfill.

API Key Lifecycle: 30-Day TTL, Refresh Tokens, and Sliding Expiry

TermOnMac's auth model uses 128-bit API keys with 30-day sliding TTLs and 512-bit refresh tokens that rotate on use, with a 5-minute grace period for client crash recovery.

QR Code as a Secure Pairing Mechanism: One-Time Token and TOFU

How TermOnMac uses a single-use QR pairing token plus a rotated room secret and TOFU identity key — so even a photographed QR code becomes useless after the legitimate pair completes.

Verifying Apple JWS Receipts Without a Third-Party Library

StoreKit 2 JWS receipts carry an x5c cert chain, not a JWK. How TermOnMac hand-rolls a DER parser to extract SPKI and verify ECDSA P-256 signatures using only Web Crypto.

Reliable Apple Subscription Notifications with Durable Objects

Three layers of protection against Apple's retrying, out-of-order App Store Server Notifications — UUID dedup, stale-downgrade rejection, and per-user DO serialization.

The R-M-W Race in Cloudflare KV: A Quota Bug and How We Fixed It

A concrete Cloudflare KV read-modify-write race that let stale Room DO flushes silently consume admin-issued quota grants — and the split-key plus version-marker fix.

Recurring Architectural Patterns in TermOnMac

Ten recurring patterns across the TermOnMac relay, Mac agent, and iOS app — strong consistency in DO, split KV keys, version markers, lazy expiry, and channel binding.

How TermOnMac Works: A Zero-Knowledge Mac Terminal Relay

How an iPhone controls a Mac terminal over the internet — iOS app, Mac CLI daemon, Cloudflare Workers relay. End-to-end encrypted; the relay forwards ciphertext it cannot read.

X25519 Key Exchange Without a Central Key Server: How QR Pairing Works

QR pairing, persistent identity keys, and per-connection ephemeral keys. How two devices derive a shared AES-256-GCM session key without a central key server.

Why We Don't Use Raw ECDH Output: HKDF Session Key Derivation

Why we don't use the raw X25519 shared secret as an AES key. HKDF-SHA256 with a versioned, nonce-salted construction gives per-connection key separation and protocol versioning.

Channel Binding with HMAC: Preventing Relay-Level MITM Attacks

An HMAC over both sorted ephemeral public keys — keyed by the room secret — prevents a compromised relay from substituting keys during the handshake.

Trust On First Use: How TermOnMac Verifies Your Mac's Identity on Reconnect

The first identity key registered for a room is trusted. How TermOnMac rotates the room secret and replaces sockets without breaking the active session.