rastrillo / native Public

Merge the native Apple chain (33ffe99..2295259) into main open

On branch merge/native-apple-chain

Paul, 2026-09-23: "Merge away." (R1 polish round, native merges.)

What. Merges the unmerged chain 33ffe99 → 6ddea4d → a49307c → 4c61d62 → cf3ea46 → 2295259 (branches native/oficina-clients, native/docs-sheets-editors, native/sparkle-updates, native/accounts-signin, all already pushed) into a branch cut from origin/main e51949f. Main previously held only CoalescedRunner; every app pinned a chain revision (four different ones).

Mid-flight check. No discussions on any of the four chain branches; last commit 2026-09-13; all chain commits are on origin. Nothing unpushed.

Conflicts. None. Merge is the chain as-is.

Extra commit. .amadan/ci.d/30-server and 40-bundle: the chain added server-test and bundle-test to make ci but CI still only ran test and companion, so the Go sign-in callback and release-tool tests would never have run in CI.

Gate. make ci locally on macOS: swift test 24 tests pass (RastrilloApple incl. accounts, HTTP, stream cancellation, origin), companion Mac + iOS xcodebuild succeed, go test -race ./server ok, python bundle/update tests OK.

After merge. Apps should re-pin to the merge commit on main (not a chain SHA). Docs/Sheets move from single-account a49307c to multi-account; Calls uses only RastrilloMacUpdates.

Needs whole-branch review (Fable) before amadan branch merge.

LLM

Brings RastrilloApple, RastrilloMacUpdates, the sign-in server and release tooling onto main so every Oficina app can pin one main revision.

CI is red for an infrastructure reason, not the merge. amadan ci status rastrillo/native -branch merge/native-apple-chain → failure at eacc073: a Linux runner claimed the job and swift test stopped fetching the Sparkle binary artifact ("could not find executable for 'unzip'"); companion/server/bundle steps never ran. Main's own tip e51949f is red the same way (companion: no xcodegen). This is #3: the repo needs amadan ci requires rastrillo/native -set "macos xcodegen" plus a macOS runner. The agent account can't set that (owner-only, per #3). Local make ci on macOS is green (see above). Needs an owner: set the requirement and run a macOS runner, then re-trigger CI on this branch.

Two things for the app re-pins after this lands:

  • Every app server still requires the Go module amadan.net/rastrillo/native/server at 33ffe99, including the multi-account testflight branches. The 2295259 client sends callback on /native/request. 33ffe99's decoder ignores the unknown field, so sign-in still works, but the browser never redirects back to the app. To get "return from system browser sign-in", each app has to go get amadan.net/rastrillo/native/server@<main merge sha>.
  • callbackURL() allows "Meet", not "Calls". Calls doesn't use this server today (it pins the kit only for RastrilloMacUpdates), so nothing breaks now.

Whole-branch review (Fable, claude-fable-5-1) — merge/native-apple-chain eacc073 vs main e51949f

VERDICT: APPROVE-WITH-NITS

Read as one diff in a scratch worktree (git diff e51949f eacc073, 52 files). The merge 3851559 is the chain 33ffe99..2295259 as-is (no conflicts), plus eacc073 adding .amadan/ci.d/30-server and 40-bundle. Per this repo's AGENTS.md this lands with amadan branch merge, not a squash.

Security review of server/

  • Proof binding: challenge = base64url(sha256(verifier)), both 32-byte canonical (valid round-trips the encoding), compared with subtle.ConstantTimeCompare; pending entries are single-use and expire after 5 minutes. Mutation: breaking the compare fails auth_test.go:87: wrong proof accepted.
  • Callback: begin accepts callback only if it equals callbackURL() exactly, an app-fixed works.oficina.<app>://signin; the approval page emits <fixed>?state=<id> where id is a validated token. No open redirect, and the URL carries no credential — the app still needs the verifier at /native/exchange. Mutation: dropping the allowlist check fails callback_test.go:38: callback "https://evil.test" status200. Client side, NativeConnection.handleOpenURL only accepts scheme/host signin, empty path, no user/port/fragment and exactly state=<pendingID>, and does nothing but cancel the browser sheet.
  • Exchange re-authenticates the approved browser cookie from a fresh context through the app's own require (so revocation/admission is re-asked) and mints a separate session for the native client; the browser cookie is never returned. /native/* sits under csrf.Protect(origin) and the client sends Origin on every request.
  • Keychain: generic-password items keyed by account:<uuid>|<origin>, kSecAttrAccessibleWhenUnlockedThisDeviceOnly, not synchronizable; legacy origin-keyed items are migrated and removed.
  • Multi-account: each account gets its own NativeHTTP (ephemeral URLSession, httpShouldSetCookies = false, urlCache = nil, explicit Cookie header, redirects refused, path locked to the origin). selectAccount replaces the client wholesale, so a request can only carry the credential of the client it was made on; cross-account leakage would have to be the consumer reusing a stale client, which Docs guards with activeAccountID == account on every callback.

Gate. make ci on this Mac, from the scratch worktree, exit 0 with every stage actually run: swift test 7 suites passed (CoalescedRunner, Configuration, HTTPStreamCancellation, HTTP, NativeAccounts, Origin, StreamLines); companion CompanionMac and CompanionIOS ** BUILD SUCCEEDED **; go test -race ./server ok; test_mac_updates.py 11 tests OK, test_bundle_mac.py 1 test OK. Hub CI red confirmed myself (amadan ci status rastrillo/native -branch merge/native-apple-chain): Linux runner, swift test dies fetching the Sparkle binary artifact ("could not find executable for 'unzip'"); infrastructure, needs the owner-only macOS requirement from #3. The two new ci.d steps are correct and match make ci.

Findings (none blocking)

  1. Low — server/auth.go callbackURL(): the allowlist is a hardcoded product list keyed on the display name and still says "Meet" (the app is Calls since 2026-09-16). Because the 2295259 client always sends a callback, any app whose name is not in that switch gets 400 Invalid proof from /native/request — sign-in fails outright, not merely "no return to app". Nothing breaks today (Calls pins the kit only for RastrilloMacUpdates), but the coupling belongs in the constructor: take the callback URL as a parameter of New and keep the exact-match check. Fix in a follow-up branch, not this merge.
  2. Low — begin: the 256-entry pending cap on an unauthenticated endpoint with a 5-minute TTL lets anyone who can reach the origin make every native sign-in answer 429 for five minutes. Fine for a one-person instance; a per-IP cap or a shorter TTL for never-approved entries would close it.
  3. Nit — NativeCredentials.save: kSecAttrAccessible* is honoured by the data-protection keychain; on macOS without kSecUseDataProtectionKeychain: true the item goes to the login keychain where the attribute is ignored (still app-ACL'd, still not synced). The sandboxed TestFlight builds have the entitlement to set it.
  4. Nit — NativeConnection.rememberSession appends a new NativeAccount on every successful sign-in, so two sign-ins to the same server as the same person produce two entries, and the older one keeps a valid session that signOut() never revokes. Dedupe on origin (plus subject when the server exposes one).
  5. Note — /native/start?id= is a link anyone can send; the defence is the code shown on both sides, which is the standard device-flow mitigation. Keep the "check that the app shows this same code" copy prominent.
  6. Nit — NativeAccountsView/NativeConnection carry hardcoded English (Button("Add account"), error messages); AGENTS.md accepts English-first with review before translating, so recording only.

Approve for amadan branch merge; apps then re-pin to the main merge commit and bump amadan.net/rastrillo/native/server.

Merging without a green hub CI run, on Paul's instruction of 2026-09-23 ("Can we merge and deploy where we're at, and continue?"). Hub CI fails only because a Linux runner claims the job (no unzip or xcodegen; main's tip is red the same way). It needs amadan ci requires rastrillo/native -set "macos xcodegen" from an owner, plus a macOS runner. Fable 5.1 approved the branch, and make ci passes on macOS with all four stages run. Landing as a merge, not a squash, per this repo's AGENTS.md.