Skip to content

MAS: let the sandbox bind the OAuth loopback callback server - #51

Merged
ptheofan merged 2 commits into
mainfrom
50-mas-loopback-entitlement
Aug 22, 2026
Merged

MAS: let the sandbox bind the OAuth loopback callback server#51
ptheofan merged 2 commits into
mainfrom
50-mas-loopback-entitlement

Conversation

@ptheofan

Copy link
Copy Markdown
Owner

Picking a Google Doc fails in the App Store build: listen EPERM on the
first try, http://localhost:0/callback…ERR_UNSAFE_PORT on every
try after that.

Root cause. resources/entitlements.mas.plist granted
com.apple.security.network.client but not
com.apple.security.network.server, so the App Sandbox denied listen().
Google's desktop flow has no alternative — custom URI schemes are no
longer supported
,
leaving loopback as the only redirect option.

Why the symptom changed on retry. startCallbackServerSync assigned
this.callbackServer before listen() could throw, and both signIn()
and pickDocument() guard startup with if (!this.callbackServer). Every
later attempt saw a live-looking server, skipped startup, and left
callbackPort at 0. The server is now assigned only once it is
genuinely listening, and the error names the entitlement.

No new provisioning profile. Decoded dist.provisionprofile carries
only application-identifier, team-identifier, application-groups
and keychain-access-groups — sandbox entitlements are not
profile-scoped, and the build already ships app-sandbox and
network.client without either appearing there.

Verification

Both tests were watched failing first, and the second reproduced the
exact production symptom: it timed out because the retry reused the
dead server, opened the browser on port 0, and waited forever for a
callback that could never arrive. After the fix it fails fast on the
second createServer call instead.

The entitlement itself is not unit-testable — it can only be confirmed by
the next TestFlight build. plutil -lint passes on the plist.

pnpm test 1033 passed / 62 files · typecheck clean · lint clean.

Closes #50

🤖 Generated with Claude Code

ptheofan and others added 2 commits August 22, 2026 14:09
Google's desktop OAuth flow redirects to a loopback HTTP server on a
random port, and custom URI schemes are no longer supported, so there is
no alternative to listening. The MAS entitlements granted network.client
but not network.server, so the App Sandbox denied listen() with EPERM and
neither sign-in nor picking a document could complete.

The failure compounded itself: the server was assigned before listen()
could throw, and both flows guard startup with `if (!this.callbackServer)`.
Every later attempt therefore saw a live-looking server, skipped startup,
and built a redirect_uri on port 0 -- which browsers refuse with
ERR_UNSAFE_PORT. The server is now assigned only once it is genuinely
listening, and the error names the missing entitlement instead of
surfacing a bare EPERM.

No new provisioning profile is needed: sandbox entitlements are not
profile-scoped. The profile carries only application-identifier,
team-identifier, application-groups and keychain-access-groups, and the
build already ships app-sandbox and network.client without either being
listed there.

Closes #50

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The helper's return annotation did not match vi.spyOn's instance type.
Caught by CI, not locally: vitest's inline type check covers less than
`tsc --noEmit` does.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@ptheofan
ptheofan merged commit ba822ba into main Aug 22, 2026
5 checks passed
@ptheofan
ptheofan deleted the 50-mas-loopback-entitlement branch August 22, 2026 11:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

MAS: Google sign-in and Doc picking fail — sandbox denies the loopback callback server

1 participant