Repository navigation
feat(cli): adapter-based APNs key setup for init apns --create-key - #1992
Merged
ArnabChatterjee20k merged 17 commits intoOct 9, 2026
Merged
ArnabChatterjee20k merged 17 commits into
ArnabChatterjee20k merged 17 commits into
Conversation
Apple has no public API for creating APNs keys, and the tools that do it (spaceship, @expo/apple-utils, Appwrite's own) all drive the Developer portal's private one, which Apple can change at any time. Each becomes an Adapter behind one contract: CreateKey checks the request, runs the adapter and checks the key it returns (key ID, team, .p8 contents), so every setup behaves the same to the caller. A Registry finds them by name. The package imports nothing from the CLI: prompts, logging, credentials and the session folder come in through the Request, so it can move to its own module unchanged. apnstest has a scripted adapter for tests.
--create-key creates the key with the setup picked by --provider, then APPWRITE_APNS_SETUP, then appwrite, so a setup Apple breaks can be swapped without a release. APPWRITE_APPLE_ID and APPWRITE_APPLE_PASSWORD answer the sign-in questions, and the CLI names the variable it used. --environment (production, sandbox or all) picks the providers to set up and the environment of a created key. Without a known team, the sign-in chooses it. The new .p8 is saved before the providers are written, since Apple allows one download, and a failed --create-key stops rather than falling back to a key already in the folder. No setup is registered yet: each comes in its own change, and until then the sign-in option is not offered.
🟢 Tier S · Ready to mergeThis PR builds on the shared APNs adapter contract and adds three key-creation backends: Appwrite’s Go-based Apple portal client, an Expo Node helper, and a fastlane lane. It registers those backends in the CLI and adds authentication, session handling, runtime checks, and tests for the adapters and Latest changes: The newest commits add and register the Appwrite, Expo, and fastlane adapters, including their Apple authentication flows and runtime-specific helpers.
📂 Walkthrough · 21
Reviewed the commits since |
A key created by the Apple sign-in has an ID Apple chose. --key-id could override it, and the providers then got an ID that does not match the .p8, so every push would fail to authenticate. --create-key now rejects --key-id, and a key created from the prompt always uses Apple's ID, with a warning when --key-id said otherwise. Sign-in questions asked without a terminal also no longer suggest --key-path, which --create-key rejects. The error names the question, says APPWRITE_APPLE_ID and APPWRITE_APPLE_PASSWORD answer the Apple ID and password, that the two-factor code always needs a terminal, and to use --key-path instead of --create-key otherwise.
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.
The expo adapter creates APNs keys with @expo/apple-utils 2.2.1 (MIT), the library eas-cli uses. A small Node script, its package.json and a lockfile pinning the package hash are embedded with go:embed and installed into the session folder with npm ci --ignore-scripts when they change. It needs Node.js 18 or later and reports apns.ErrUnavailable otherwise, before contacting Apple. apple-utils prompts on the terminal for the two-factor code and for anything the request leaves out; the request's Apple ID and password are passed as EXPO_APPLE_ID and EXPO_APPLE_PASSWORD, and EXPO_NO_KEYCHAIN=1 keeps the password out of the macOS Keychain. Its InvalidUserCredentialsError maps to apns.ErrInvalidCredentials and a full team to apns.MaxKeysError. apple-utils always creates keys for both environments, which also serve a single-environment request.
Registers the expo adapter, picked with --provider expo or APPWRITE_APNS_SETUP=expo.
The fastlane adapter creates APNs keys with spaceship, fastlane's open-source Apple Developer portal client, through the fastlane the person already has installed: a lane in an embedded Fastfile runs in a private temp folder, so a gem, Homebrew or bundler installation all work and no Ruby ships with the CLI. It needs fastlane 2.226.0 or later, the first to sign in with SRP, and reports apns.ErrUnavailable otherwise. The adapter asks for the Apple ID and password when the request has none and passes them as FASTLANE_USER and FASTLANE_PASSWORD, so fastlane never offers to store them in the Keychain; spaceship asks for the two-factor code on the terminal. The lane calls create_key! directly so the requested environment is honoured (Key.create always asks for all), maps InvalidUserCredentialsError to apns.ErrInvalidCredentials and a full team to apns.MaxKeysError with the team's keys.
Registers the fastlane adapter, picked with --provider fastlane or APPWRITE_APNS_SETUP=fastlane.
2.225.0 (October 2024) is the release that added SRP sign-in, per its notes: earlier versions get 503 from Apple. The minimum was set one release too high.
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.
The fastlane setup ran fastlane from PATH in a temporary folder, so a fastlane installed only through a project's Gemfile was reported as unavailable. When the Gemfile in the folder the CLI runs in, or in its ios folder as Flutter and React Native projects keep it, lists fastlane and bundle is installed, the lane now runs with bundle exec fastlane and BUNDLE_GEMFILE pointing at that Gemfile. Otherwise fastlane from PATH is used as before. A Bundler fastlane that does not start asks for bundle install.
The CLI leaves the team out when none is known and lets the sign-in pick it. The test now checks that the helper then gets no --team-id.
When the project's Gemfile lists fastlane but Bundler is missing, the fastlane setup fell back to fastlane from PATH, which can be a different version from the pinned one. It now reports apns.ErrUnavailable naming the Gemfile and how to install Bundler.
…g-provider-cli-apns-adapters # Conflicts: # src/SDK/Language/CLI.php # templates/cli/internal/cmd/initpush.go
…aging-provider-cli-apns-adapters # Conflicts: # src/SDK/Language/CLI.php # templates/cli/internal/cmd/initpush.go
ArnabChatterjee20k
merged commit Oct 9, 2026
53d63b3
into
messaging-provider-cli-apns
53 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 #1963 (
init apns). This is the base for one-click APNs key creation. The adapters come in separate PRs on top of it:@expo/apple-utilsthrough an embedded Node helper, from feat(cli): init apns --create-key creates the APNs key with the Apple ID #1989.Why adapters
Apple has no public API for creating APNs keys. spaceship,
@expo/apple-utilsand our own implementation all sign in as a person would and call the Developer portal's private API, which Apple changes without notice. With adapters, picking the implementation is a setting, not a release: when Apple breaks one, users (or our docs) switch to another.Usage
From the CLI
How the setup is chosen:
--provider, thenAPPWRITE_APNS_SETUP, thenappwrite.Two-factor still needs a person: the code is asked in the terminal. A setup that keeps a session (stored under
~/.appwrite/apns/<setup>/) skips it on later runs.What happens when key creation fails:
--create-keyand the sign-in or key creation fails.p8with--key-path, or revoke one--provider/APPWRITE_APNS_SETUPFrom Go
Creating a key with a setup:
Writing a setup:
Then add it in one place,
apnsAdapters()ininternal/cmd/initpush.go:apns.CreateKeychecks what every setup returns: a 10-character key ID, a valid team ID that matches the requested team, and.p8contents. A setup that breaks this contract fails loudly instead of writing a bad provider.apnstest.Adapteris a scripted fake for testing code that uses a setup.What's here
internal/apns(no imports from the CLI, so it can move to its own module and repo):Adapterinterface:Name()andCreateKey(ctx, Request) (Key, error).Request: team (optional), key name, environment, optional Apple ID and password, session folder, and theAsker/Logcallbacks for prompts and output.Key,ExistingKey,MaxKeysError,ErrUnavailable,ErrInvalidCredentials,Environment/ParseEnvironment.CreateKey(ctx, adapter, request)holds every adapter to the same contract. It checks the request (environment, name, team ID format), then the result: a valid key ID and team, the requested team, and.p8contents.Registry: adapters by name, with clear errors for an unknown name or an empty build.apnstest.Adapter: a scripted fake for tests.init apns:--create-keycreates the key with the chosen setup:--provider appwrite|expo|fastlane, thenAPPWRITE_APNS_SETUP, thenappwrite. The flag + env pattern matches--config-file/APPWRITE_CONFIG_FILE.~/.appwrite/apns/<setup>/.APPWRITE_APPLE_ID/APPWRITE_APPLE_PASSWORDanswer the sign-in questions. The CLI prints the variable name, never the value.--environment production|sandbox|all(defaultall) picks which providers are set up, what counts as "already set up", and the environment of a created key..p8is saved (0600, never overwriting) before the providers are written, because Apple allows one download.--key-pathadvice.--create-keystops instead of falling back to a.p8already in the folder. Choosing the sign-in from the prompt still falls back to the other options.--create-keysays the build has none. Each adapter PR adds itself toapnsAdapters().Tests
internal/apns: environment parsing, request checks (the adapter never runs on a bad request), the result contract (bad key ID, missing team, wrong team), error pass-through, registry lookup and errors.init apns(through the fake adapter):--providerbeatsAPPWRITE_APNS_SETUP, which beats the default; credentials passed through without the password ever printed-race), gofmt, Rector and the Generation suite pass.Supersedes
#1989 and #1991 once the expo and appwrite adapter PRs land on this.