Repository navigation
feat(cli): appwrite APNs key setup (native Go Apple sign-in) - #1993
Merged
ArnabChatterjee20k merged 4 commits intoOct 9, 2026
Conversation
The appwrite adapter signs in to Apple and calls the Developer portal in Go, with nothing to install. The sign-in follows spaceship: the widget key from the App Store Connect sign out redirect, SRP-6a against idmsa.apple.com (PBKDF2 over the password, s2k and s2k_fo), hashcash, two-factor by device code or text message, and a trusted session kept in the request's session folder so later runs skip two-factor. The key calls follow @expo/apple-utils: list the team's keys, which hands out the CSRF tokens, create a team-scoped key with APNs for the requested environment, and download the .p8. The SRP and hashcash code is checked against values computed by fastlane-sirp and spaceship, and the whole flow against a fake Apple that runs the server side of SRP. The Go bodies of portal.go and adapter_test.go are Twig verbatim blocks, as their nested composite literals read as Twig tags.
Registers the appwrite adapter, the default for --create-key, so the Apple sign-in is offered and --provider appwrite (or APPWRITE_APNS_SETUP=appwrite) selects it.
🟢 Tier S · Ready to mergeAdds a native Go APNs key-creation adapter that signs in to Apple, handles two-factor authentication, and creates and downloads team-scoped keys. Registers the adapter as the CLI’s default setup and adds fake-server coverage for sign-in, session reuse, team selection, and key creation.
Note Not approving while a bug finding is open: Don't count choosing SMS as a code attempt 📂 Walkthrough · 9
Reviewed |
This was referenced Oct 9, 2026
Typing sms to get the code by text message used one of the three attempts, leaving two for actual codes. Only codes Apple rejects count now.
The hashcash date was the machine's local time. @expo/apple-utils sends UTC (spaceship's local time only matches on a UTC machine), so a non-UTC machine produced a date hours away from the one Apple expects.
ArnabChatterjee20k
merged commit Oct 9, 2026
f900aaa
into
messaging-provider-cli-apns-adapters
54 of 61 checks passed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #1992 (the APNs key setup adapters). This is the appwrite adapter, the default for
init apns --create-key. It replaces #1991, which had the same code before the adapter split.What
internal/apns/appwrite: anapns.Adapternamedappwritethat signs in to Apple and creates the key in Go, with no Node.js or Ruby to install. It's registered inapnsAdapters(), so the Apple sign-in option is offered and--provider appwrite/APPWRITE_APNS_SETUP=appwriteselect it. It's also the default when neither is set.How
Apple has no public API for creating APNs keys, so this does what the portal's web pages do. The sign-in follows fastlane's spaceship (readable, MIT, kept current), and the key calls follow
@expo/apple-utils2.2.1.Sign-in (
auth.go,srp.go):HEAD appstoreconnect.apple.com/logout(thewidgetKeyin the redirect), sent without cookies and without following the redirect. It falls back to the copy saved by an earlier run, then the oldolympus/v1/app/config.POST idmsa.apple.com/appleauth/auth/signin/init, then PBKDF2-SHA256 over the password's SHA-256 (hex-encoded first fors2k_fo), thensignin/completewithm1/m2. The password never leaves the machine.GET /appleauth/auth/signin?widgetKey=…, sent asX-Apple-HC.sms, or automatically when the account has no trusted devices). The code is checked withPOST …/verify/{trusteddevice|phone}/securitycode, with up to three tries, thenGET /appleauth/auth/2sv/trust.GET olympus/v1/sessionconfirms it.<SessionDir>/session.json(0600) as soon as Apple accepts the code, and reused on later runs.Keys (
portal.go), underdeveloper.apple.com/services-account/QH65B2/:account/listTeams.action: the requested team must be on the account. One team is used directly, several are offered as a choice.account/auth/key/list: also hands out thecsrf/csrf_tstokens the create needs.account/auth/key/v2/createwithscope: team,serviceConfigurations: {U27F4V844T: []}and one APNs request for the requested environment (all,productionorsandbox).account/auth/key/downloadreturns the.p8.apns.MaxKeysErrorwith the team's keys. A refused sign-in becomesapns.ErrInvalidCredentials.The adapter reads no environment variables. The Apple ID and password come in the request, filled by the CLI from
APPWRITE_APPLE_ID/APPWRITE_APPLE_PASSWORD.Correctness
srp_test.gochecks the PBKDF2 key,A,m1andm2for boths2kands2k_foagainst values computed byfastlane-sirp(the gem spaceship uses), and hashcash against spaceship's. These include the non-obvious parts:xhas no username;A,Sandm2drop leading zero bytes;kandupad to 256 bytes.adapter_test.goruns the whole flow against a fake Apple that does the server side of SRP:Real-account status
Against a real Apple account, the SRP sign-in was accepted: Apple moved on to two-factor and texted a code. Creating a key end to end, and the
production/sandboxenvironment values (spaceship and apple-utils only ever sendall), still need a full run.Notes
portal.goandadapter_test.gowrap their Go bodies in Twig{% verbatim %}, because nested composite literals ({{ … }}) read as Twig tags in templated files. The other files are copied as they are, or only use the module path.QH65B2/, notv1/.GET /appleauth/auth/signin.keyId/keyName.