App Store Connect

@crvouga/mockingbird-service-app-store-connect

App Store Connect JSON:API emulator with verified ES256 JWTs and TestFlight membership state.

Parity: JWT-authenticated apps, users, invitations and TestFlight groupsIdentity14 of 14 operationsIn-process fetchNode servermockingbird-app-store-connect
$
npm install -D @crvouga/mockingbird-service-app-store-connect

Playground

Requests go to the real emulator running in this tab. State persists across requests, and the journal shows what it received. A green dot marks operations whose sample succeeds as-is; others need ids from earlier responses.

Loads the real @crvouga/mockingbird-service-app-store-connect into this tab on first send
https://api.appstoreconnect.apple.com

Send a request to see the emulator's response.

Operations

14 of 14 operations in the vendored contract are emulated.

MethodPathOperationStatus
GET/v1/appsListApps
GET/v1/usersListUsers
GET/v1/userInvitationsListInvitations
POST/v1/userInvitationsCreateInvitation
GET/v1/betaGroupsListGroups
POST/v1/betaGroupsCreateGroup
GET/v1/betaTestersListTesters
POST/v1/betaTestersCreateTester
GET/v1/betaGroups/{id}/betaTestersListGroupTesters
POST/v1/betaGroups/{id}/relationships/betaTestersAddGroupTesters
DELETE/v1/betaGroups/{id}/relationships/betaTestersRemoveGroupTesters
POST/v1/users/{id}/relationships/visibleAppsAddVisibleApps
DELETE/v1/users/{id}DeleteUser
DELETE/v1/userInvitations/{id}DeleteInvitation

Documentation

The package README, the same file that ships in the npm tarball.Edit on GitHub

Local emulators. Real API contracts. Part of Mockingbird.

WIP App Store Connect emulator for JSON:API apps, users, user invitations, beta groups and beta testers. ES256 signatures are actually verified; no real Apple account or emails.

Install

Shell
bun add -d @crvouga/mockingbird-service-app-store-connect

Usage

TypeScript
import { SignJWT } from "jose"
import { createServer } from "@crvouga/mockingbird-service-app-store-connect/server"

// Test setup: a throwaway ES256 key pair. The emulator only gets the public half.
// Never use a real Apple private key here.
const pair = await crypto.subtle.generateKey(
  { name: "ECDSA", namedCurve: "P-256" }, true, ["sign", "verify"],
)
const server = await createServer({
  keys: [{ id: "mock-key", issuer: "mock-issuer", role: "ADMIN",
    publicKey: await crypto.subtle.exportKey("jwk", pair.publicKey) }],
})

// Apple publishes no JavaScript SDK, so the application signs its team JWT and calls the API
// over HTTP. Only the origin changes: server.url instead of api.appstoreconnect.apple.com.
const token = await new SignJWT({})
  .setProtectedHeader({ alg: "ES256", kid: "mock-key", typ: "JWT" })
  .setIssuer("mock-issuer")
  .setAudience("appstoreconnect-v1")
  .setIssuedAt()
  .setExpirationTime("10m")
  .sign(pair.privateKey)
const apps = await fetch(`${server.url}/v1/apps`, { headers: { authorization: `Bearer ${token}` } })
const { data } = (await apps.json()) as { data: { attributes: { name: string } }[] }
console.log(data[0]?.attributes.name) // Synthetic App

await server.close()

createRuntime().fetch(Request) supports in-process clients. The CLI starts without trusted keys; seed public verification keys through /__admin/state/keys, or use server options. There is no static bypass bearer token. Private signing material is never needed by the emulator; options containing a private JWK are rejected. Generate test signing keys at test startup.

Authentication

Team JWTs require ES256, typ: JWT, registered kid, matching issuer and appstoreconnect-v1 audience, valid iat/exp, signature verification and a lifetime at most 20 minutes. The emulator clock determines expiry. enabled: false revokes a seeded key. Optional scope claims restrict GET paths/query parameters; limit/cursor/sort are ignored for matching as documented by Apple. Invalid authentication returns JSON:API 401 before mutation.

The scoped write-role policy follows Apple's team/TestFlight guides: ADMIN/ACCOUNT_HOLDER may write all supported surfaces; APP_MANAGER may manage beta groups/testers and create user invitations. Other seeded roles are denied writes with 403. Read authorization and app-specific role restrictions are simplified; see limitations below.

Routes

  • GET /v1/apps, /v1/users, /v1/userInvitations, /v1/betaGroups, /v1/betaTesters.
  • POST /v1/userInvitations, /v1/betaGroups, /v1/betaTesters.
  • GET /v1/betaGroups/{id}/betaTesters.
  • POST/DELETE /v1/betaGroups/{id}/relationships/betaTesters.
  • POST /v1/users/{id}/relationships/visibleApps.
  • DELETE /v1/users/{id} and /v1/userInvitations/{id}.

Lists support limit/cursor, sparse fields[type], name/email filters and the requested app filter for groups/testers. links.next remains on the configured local origin and keeps the public namespace. Relationship writes validate every target before changing anything; removing a tester membership does not delete the tester, group or application user. Duplicate tester/invitation emails return a 409 JSON:API error and can also be fault-injected.

Controls

Seed resources as JSON:API records and keys as public verification keys. Seeded group/tester and user/visibleApp relationships initialize the durable edge collections. /__admin/state exposes resources, keys, memberships, visibleApps for synthetic fixtures. Standard reset, Timeline snapshots/branches, clock, request journal and faults are available. Requests use header, /__admin/ns/<name>/… path or mapped bearer credential namespaces; adminPrefix relocates the reserved tree. Journals hold no JWTs or request bodies. Presets: unauthorized, forbidden, duplicate, rate_limited, server_error, connection_drop; generic faults also support deterministic latency. No webhooks are emitted.

Verification

bun test verifies all four reported behaviors through raw fetch, actual signatures using independently generated jose ES256 keys, JWT rejection/revocation, scoped writes, seeded edges, served HTTP, namespace/reset/fault controls, OpenAPI random walks and deliberate divergence. The consumer is a port of the issue's reported surface, not unavailable private app code. No official SDK was requested. No live account credentials or writes were used. APP_STORE_CONNECT_TOKEN=… bun run parity safely probes a single app-list response shape; missing credentials exit 2. It is not a full vendor random-walk parity claim.

Contract sources: Apple's official OpenAPI download, JWT guide, team guide, and external testers guide. Request schemas are retained from the published OpenAPI; response fields are a scoped subset.

Deliberately not modelled

Binary uploads, App Store release, real invitation emails, builds, device installation, TestFlight review prerequisites and user acceptance of invitations. Individual-key JWTs and long-lived tokens for unrelated Apple APIs are outside this team-key surface. Fine-grained read permissions, per-app key access, advanced role assignment restrictions, relationship expansion/include and production duplicate diagnostic variants are not guaranteed. Admin fixture edits can construct states the live API would forbid; seed coherent records.

API

Root runtime exports: AppStoreConnectAPI, APP_STORE_CONNECT_NAMESPACE, DEFAULT_RESOURCES, createRuntime, APP_STORE_CONNECT_PRESETS, document, operationIds, supportedOperationIds. Types: ApiKey, ResourceType, Linkage, Resource, AppStoreConnectAPIOptions, AppStoreConnectRuntimeOptions, AppStoreConnectRuntime, OperationId, SupportedOperationId. The /server entry exports createServer, serveTarget, DEFAULT_PORT.

↑↓ to navigate↵ to open