Skip to content

feat(cli): appwrite APNs key setup (native Go Apple sign-in) - #1993

Merged
ArnabChatterjee20k merged 4 commits into
messaging-provider-cli-apns-adaptersfrom
messaging-provider-cli-apns-adapter-appwrite
Oct 9, 2026
Merged

ArnabChatterjee20k merged 4 commits into
messaging-provider-cli-apns-adaptersfrom
messaging-provider-cli-apns-adapter-appwrite

Conversation

@ArnabChatterjee20k

Copy link
Copy Markdown
Member

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: an apns.Adapter named appwrite that signs in to Apple and creates the key in Go, with no Node.js or Ruby to install. It's registered in apnsAdapters(), so the Apple sign-in option is offered and --provider appwrite / APPWRITE_APNS_SETUP=appwrite select it. It's also the default when neither is set.

key, err := apns.CreateKey(ctx, appwrite.New(), apns.Request{
    Name:        "Appwrite Push",
    Environment: apns.EnvironmentAll,
    SessionDir:  "~/.appwrite/apns/appwrite",
    Asker:       asker,
})

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-utils 2.2.1.

Sign-in (auth.go, srp.go):

  1. Widget key: from HEAD appstoreconnect.apple.com/logout (the widgetKey in the redirect), sent without cookies and without following the redirect. It falls back to the copy saved by an earlier run, then the old olympus/v1/app/config.
  2. SRP-6a: POST idmsa.apple.com/appleauth/auth/signin/init, then PBKDF2-SHA256 over the password's SHA-256 (hex-encoded first for s2k_fo), then signin/complete with m1/m2. The password never leaves the machine.
  3. Hashcash: from GET /appleauth/auth/signin?widgetKey=…, sent as X-Apple-HC.
  4. Two-factor (on 409): the code pushed to trusted devices, or a text message (type sms, or automatically when the account has no trusted devices). The code is checked with POST …/verify/{trusteddevice|phone}/securitycode, with up to three tries, then GET /appleauth/auth/2sv/trust.
  5. Session: GET olympus/v1/session confirms it.
    • The cookies are saved to <SessionDir>/session.json (0600) as soon as Apple accepts the code, and reused on later runs.
    • When the session expires, its trust cookie is kept, so signing in again skips two-factor, as spaceship does.
    • A saved session for a different Apple ID than the one requested is not reused.

Keys (portal.go), under developer.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 the csrf / csrf_ts tokens the create needs.
  • account/auth/key/v2/create with scope: team, serviceConfigurations: {U27F4V844T: []} and one APNs request for the requested environment (all, production or sandbox).
  • account/auth/key/download returns the .p8.
  • "maximum allowed number of Keys" becomes apns.MaxKeysError with the team's keys. A refused sign-in becomes apns.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.go checks the PBKDF2 key, A, m1 and m2 for both s2k and s2k_fo against values computed by fastlane-sirp (the gem spaceship uses), and hashcash against spaceship's. These include the non-obvious parts: x has no username; A, S and m2 drop leading zero bytes; k and u pad to 256 bytes.
  • adapter_test.go runs the whole flow against a fake Apple that does the server side of SRP:
    • two-factor with a wrong code first
    • a text-message code
    • session reuse with no questions and no sign-in requests
    • saving the session when App Store Connect then refuses it
    • signing in again after expiry without a code (the quoted trust cookie survives a save and load)
    • credentials from the request
    • a wrong password (no session saved)
    • a full team
    • team choice and an unknown team
    • a single environment
    • the widget-key fallback

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 / sandbox environment values (spaceship and apple-utils only ever send all), still need a full run.

Notes

  • portal.go and adapter_test.go wrap 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.
  • Corrections to the earlier reverse-engineering notes:
    • Cookie portal calls use QH65B2/, not v1/.
    • Hashcash comes from GET /appleauth/auth/signin.
    • Codes are verified with POST.
    • Key fields are keyId / keyName.

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.
@hansi-codes

hansi-codes Bot commented Oct 9, 2026 •

Copy link
Copy Markdown

🟢 Tier S · Ready to merge

Adds 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.

Verdict New comments Fixed Still open
💬 Commented 0 0 0

Note

Not approving while a bug finding is open: Don't count choosing SMS as a code attempt

📂 Walkthrough · 9
File Change
src/SDK/Language/CLI.php Registers the Appwrite APNs adapter source and test files for CLI generation.
templates/cli/internal/apns/appwrite/adapter.go Implements adapter setup, saved-session reuse, and team selection.
templates/cli/internal/apns/appwrite/adapter_test.go Exercises sign-in, two-factor, session reuse, team selection, and key creation against a fake Apple service.
templates/cli/internal/apns/appwrite/auth.go Implements Apple sign-in, widget-key retrieval, hashcash, and two-factor flows.
templates/cli/internal/apns/appwrite/jar.go Adds a persistent cookie jar for Apple sessions.
templates/cli/internal/apns/appwrite/portal.go Implements Apple Developer portal team and APNs key operations.
templates/cli/internal/apns/appwrite/srp.go Implements SRP proof generation and hashcash.
templates/cli/internal/apns/appwrite/srp_test.go Checks SRP and hashcash results against fixed reference vectors.
templates/cli/internal/cmd/initpush.go Registers the Appwrite adapter in the CLI and makes it the default.

Reviewed a02c29b · Details · Comment @hansi-codes review to re-run, or mention @hansi-codes with a question.

@hansi-codes hansi-codes Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Tier A · See the inline comments. Summary

Comment thread templates/cli/internal/apns/appwrite/auth.go Outdated
Comment thread templates/cli/internal/apns/appwrite/srp.go Outdated
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.

@hansi-codes hansi-codes Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Tier S · Looks good to merge. Summary

@ArnabChatterjee20k
ArnabChatterjee20k merged commit f900aaa into messaging-provider-cli-apns-adapters Oct 9, 2026
54 of 61 checks passed
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.

1 participant