Repository navigation
feat(cli): create APNs keys with a native Apple sign-in instead of Node.js - #1991
Closed
ArnabChatterjee20k wants to merge 6 commits into
Closed
ArnabChatterjee20k wants to merge 6 commits into
ArnabChatterjee20k wants to merge 6 commits into
Conversation
Apple signs in with SRP-6a over the RFC 5054 2048-bit group and SHA-256, with PBKDF2 over the password (hex-encoded first for s2k_fo), and asks for a SHA-1 hashcash on the final step. This ports fastlane-sirp and spaceship's hashcash, which spaceship signs in with, and checks them against values those Ruby implementations computed for fixed inputs.
…de.js init apns --create-key no longer needs Node.js or @expo/apple-utils. The CLI signs in to Apple itself, as spaceship does: the widget key from the App Store Connect sign out redirect, SRP against idmsa.apple.com, hashcash, two-factor by device code or text message, and a trusted session saved in ~/.appwrite/apple so later runs skip two-factor. It then calls the Developer portal's key API the way @expo/apple-utils does: list the team's keys (which hands out the CSRF tokens), create a team-scoped key with APNs for all environments, and download the .p8. The Apple ID and password can come from APPWRITE_APPLE_ID and APPWRITE_APPLE_PASSWORD, or EXPO_APPLE_ID and EXPO_APPLE_PASSWORD for anyone set up for eas-cli; the CLI names the variable it used. The questions go through the CLI's prompts.
APPWRITE_APPLE_ID and APPWRITE_APPLE_PASSWORD answer the sign-in questions. The EXPO_APPLE_* fallback is gone, so a variable set up for eas-cli never signs in to Apple with an account nobody picked here.
The session was saved only after App Store Connect confirmed it, so a failure after two-factor cost another code on the next run. It is now saved as soon as Apple accepts the sign-in. An expired session's cookies are also kept when signing in again, as spaceship does: the one that marks the machine as trusted lets Apple skip two-factor.
After a failed Apple sign-in, init apns --create-key fell back to a .p8 already in the project folder and set the providers up with it, so a run asked for a new key reported success with the old one. It now stops with the reason. Choosing the Apple sign-in from the prompt still falls back to the other ways of providing the key.
--environment (production, sandbox or all, the default) decides which Appwrite APNs providers are set up, which ones must exist for the command to report APNs already set up, and, with --create-key, which environment the new Apple key works in. Spaceship and apple-utils only ever send "all"; "production" and "sandbox" are the values the portal's choices stand for.
This was referenced Oct 9, 2026
Member
Author
|
Closing in favour of the adapter split. Everything here is carried over:
The branch is kept. |
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 #1989. Draft until it has signed in to a real Apple account.
What
appwrite init apns --create-keyno longer needs Node.js or@expo/apple-utils. The CLI signs in to Apple and calls the Developer portal itself, in Go.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 (
internal/apple/auth.go,srp.go)HEAD appstoreconnect.apple.com/logout(thewidgetKeyin the redirect), sent without cookies and without following the redirect. Falls back to the copy saved by an earlier run, then the oldolympus/v1/app/config(removed by Apple in September 2026).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=…(X-Apple-HC-Bits/X-Apple-HC-Challenge), sent asX-Apple-HC.GET /appleauth/authfor the options, then the code pushed to trusted devices, or a text message (typingsms, or automatically when the account has no trusted devices),POST …/verify/{trusteddevice|phone}/securitycode. A wrong code is asked again up to three times. ThenGET /appleauth/auth/2sv/trust.GET olympus/v1/sessionconfirms the session. Cookies are saved to~/.appwrite/apple/session.json(0600), so later runs skip the sign-in. A saved session for a different Apple ID than the one in the environment is not reused.Keys (
portal.go), underdeveloper.apple.com/services-account/QH65B2/:account/listTeams.action: 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 forenvironment: all.account/auth/key/downloadreturns the.p8.MaxKeysError, which lists the team's keys.Credentials:
APPWRITE_APPLE_ID/APPWRITE_APPLE_PASSWORDonly, so a variable set up for another tool never signs in by accident. The CLI prints which variable it used, never the value. Otherwise it asks through the CLI's prompts, with the password hidden.Environment:
--environment production|sandbox|all(defaultall) decides which Appwrite APNs providers are set up and must exist for "already set up", and, with--create-key, the environment the Apple key works in. spaceship and apple-utils only ever send"all";"production"and"sandbox"are unconfirmed until a real run.Failures: with
--create-key, a failed sign-in or key creation stops the command with the reason instead of falling back to a.p8in the project folder. Choosing the Apple sign-in from the prompt still falls back to the other options. The session is saved as soon as Apple accepts the code, and an expired session's trust cookie is kept so signing in again skips two-factor.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.apple_test.goruns the whole flow against a fake Apple that does the server side of SRP: two-factor with a wrong code first, session reuse with no questions or sign-in requests, credentials from the environment (password never logged), wrong password (no session saved), full team, team choice and an unknown team, and widget-key fallback.init apnstests are unchanged apart from the fake no longer being a Node error.Not yet verified
Notes against the reverse-engineering doc
Checked against the sources, a few details differ from the first write-up:
services-account/QH65B2/.services-account/v1/is for the token client.GET /appleauth/auth/signin?widgetKey=, not from thesignin/initresponse.keyId/keyName.