defmodule AppleDeveloper do @moduledoc """ Elixir client for the [Apple Developer API](https://developer.apple.com/documentation/appstoreconnectapi). Provides access to: - Certificates (development, distribution, push notification) - Provisioning profiles - Devices (UDID management) - App Store Connect apps and builds The public surface is intentionally small: AppleDeveloper.list_certificates() AppleDeveloper.create_certificate(:ios_development, csr_content) AppleDeveloper.revoke_certificate("certificate_id") AppleDeveloper.list_devices() AppleDeveloper.register_device("iPhone 14", "A1B2C3D4...", "IOS") AppleDeveloper.list_profiles() AppleDeveloper.create_profile("profile_name", "bundle_id", "certificate_id", ["device_id"]) AppleDeveloper.token() ## Configuration config :apple_developer, issuer_id: System.get_env("APPLE_ISSUER_ID"), key_id: System.get_env("DEVELOPER_KEY_ID"), private_key: System.get_env("DEVELOPER_PRIVATE_KEY"), base_url: "https://api.appstoreconnect.apple.com" Every function also accepts per-call `opts` that override the application config. """ alias AppleDeveloper.{Client, Token} @type opts :: keyword() @type response :: {:ok, map()} | {:error, term()} @doc "Return a cached-per-call App Store Connect **access token** (after the JWT generation)." @spec token(opts) :: {:ok, String.t()} | {:error, term()} def token(opts \\ []), do: Token.access_token(opts) # Certificates @doc """ List all certificates for your team. ## Parameters - `opts`: - `:filter_serial` - Filter by serial number - `:filter_status` - Filter by status ("ACTIVE", "EXPIRED", "REVOKED") - `:limit` - Maximum results (default 200) """ @spec list_certificates(opts) :: {:ok, list(map())} | {:error, term()} def list_certificates(opts \\ []) do Client.get("/v1/certificates", opts) end @doc """ Get a specific certificate by ID. ## Parameters - `certificate_id`: The certificate ID - `opts`: Optional overrides """ @spec get_certificate(String.t(), opts) :: response def get_certificate(certificate_id, opts \\ []) when is_binary(certificate_id) do Client.get("/v1/certificates/#{certificate_id}", opts) end @doc """ Create a new certificate. ## Parameters - `certificate_type`: One of: - `:ios_development` - `:ios_distribution` - `:mac_app_distribution` - `:mac_installer_distribution` - `:mac_app_development` - `:developer_id_kext` - `:developer_id_application` - `csr_content`: The Certificate Signing Request content (PEM format) - `opts`: Optional overrides ## Examples AppleDeveloper.create_certificate(:ios_development, csr_content) """ @spec create_certificate(atom(), String.t(), opts) :: response def create_certificate(certificate_type, csr_content, opts \\ []) when is_atom(certificate_type) and is_binary(csr_content) do type_string = certificate_type |> Atom.to_string() |> String.upcase() body = %{ data: %{ type: "certificates", attributes: %{ certificateType: type_string, csrContent: csr_content } } } Client.post("/v1/certificates", body, opts) end @doc """ Revoke a certificate. ## Parameters - `certificate_id`: The certificate ID to revoke - `opts`: Optional overrides """ @spec revoke_certificate(String.t(), opts) :: :ok | {:error, term()} def revoke_certificate(certificate_id, opts \\ []) when is_binary(certificate_id) do case Client.delete("/v1/certificates/#{certificate_id}", opts) do {:ok, _} -> :ok error -> error end end # Devices @doc """ List all registered devices. ## Parameters - `opts`: - `:filter_status` - Filter by status ("ENABLED", "DISABLED") - `:filter_platform` - Filter by platform ("IOS", "MAC_OS") - `:filter_udi` - Filter by UDID - `:limit` - Maximum results """ @spec list_devices(opts) :: {:ok, list(map())} | {:error, term()} def list_devices(opts \\ []) do Client.get("/v1/devices", opts) end @doc """ Get a specific device by ID. ## Parameters - `device_id`: The device ID - `opts`: Optional overrides """ @spec get_device(String.t(), opts) :: response def get_device(device_id, opts \\ []) when is_binary(device_id) do Client.get("/v1/devices/#{device_id}", opts) end @doc """ Register a new device for development. ## Parameters - `name`: Device name/label - `udid`: Device UDID (unique device identifier) - `platform`: Platform ("IOS" or "MAC_OS") - `opts`: Optional overrides ## Examples AppleDeveloper.register_device("iPhone 14 Pro", "00008110-001234567890ABCDE", "IOS") """ @spec register_device(String.t(), String.t(), String.t(), opts) :: response def register_device(name, udid, platform, opts \\ []) when is_binary(name) and is_binary(udid) do body = %{ data: %{ type: "devices", attributes: %{ name: name, udid: udid, platform: platform } } } Client.post("/v1/devices", body, opts) end @doc """ Update a device name. ## Parameters - `device_id`: The device ID - `new_name`: New device name - `opts`: Optional overrides """ @spec update_device(String.t(), String.t(), opts) :: response def update_device(device_id, new_name, opts \\ []) when is_binary(device_id) and is_binary(new_name) do body = %{ data: %{ type: "devices", id: device_id, attributes: %{ name: new_name } } } Client.patch("/v1/devices/#{device_id}", body, opts) end # Provisioning Profiles @doc """ List all provisioning profiles. ## Parameters - `opts`: - `:filter_profile_state` - Filter by state ("ACTIVE", "EXPIRED", "INVALID") - `:filter_profile_type` - Filter by type - `:limit` - Maximum results """ @spec list_profiles(opts) :: {:ok, list(map())} | {:error, term()} def list_profiles(opts \\ []) do Client.get("/v1/profiles", opts) end @doc """ Get a specific profile by ID. ## Parameters - `profile_id`: The profile ID - `opts`: Optional overrides """ @spec get_profile(String.t(), opts) :: response def get_profile(profile_id, opts \\ []) when is_binary(profile_id) do Client.get("/v1/profiles/#{profile_id}", opts) end @doc """ Create a new provisioning profile. ## Parameters - `name`: Profile name - `profile_type`: Profile type (e.g., "IOS_APP_DEVELOPMENT", "IOS_APP_STORE") - `bundle_id`: The bundle ID to associate - `certificate_ids`: List of certificate IDs - `device_ids`: List of device IDs (for development profiles) - `opts`: Optional overrides ## Examples AppleDeveloper.create_profile( "My Development Profile", "IOS_APP_DEVELOPMENT", "bundle_id_123", ["cert_id_456"], ["device_id_789"] ) """ @spec create_profile(String.t(), String.t(), String.t(), [String.t()], [String.t()], opts) :: response def create_profile(name, profile_type, bundle_id, certificate_ids, device_ids, opts \\ []) when is_binary(name) and is_binary(profile_type) and is_binary(bundle_id) and is_list(certificate_ids) do body = %{ data: %{ type: "profiles", attributes: %{ name: name, profileType: profile_type }, relationships: %{ bundleId: %{ data: %{type: "bundleIds", id: bundle_id} }, certificates: %{ data: Enum.map(certificate_ids, &%{type: "certificates", id: &1}) }, devices: %{ data: Enum.map(device_ids, &%{type: "devices", id: &1}) } } } } Client.post("/v1/profiles", body, opts) end @doc """ Delete a provisioning profile. ## Parameters - `profile_id`: The profile ID to delete - `opts`: Optional overrides """ @spec delete_profile(String.t(), opts) :: :ok | {:error, term()} def delete_profile(profile_id, opts \\ []) when is_binary(profile_id) do case Client.delete("/v1/profiles/#{profile_id}", opts) do {:ok, _} -> :ok error -> error end end # Bundle IDs @doc """ List all bundle IDs. ## Parameters - `opts`: - `:filter_identifier` - Filter by bundle identifier - `:limit` - Maximum results """ @spec list_bundle_ids(opts) :: {:ok, list(map())} | {:error, term()} def list_bundle_ids(opts \\ []) do Client.get("/v1/bundleIds", opts) end @doc """ Get a specific bundle ID. ## Parameters - `bundle_id`: The bundle ID resource ID - `opts`: Optional overrides """ @spec get_bundle_id(String.t(), opts) :: response def get_bundle_id(bundle_id, opts \\ []) when is_binary(bundle_id) do Client.get("/v1/bundleIds/#{bundle_id}", opts) end @doc """ Register a new bundle ID. ## Parameters - `identifier`: The bundle identifier (e.g., "com.example.app") - `name`: Display name for the bundle ID - `platform`: Platform ("IOS", "MAC_OS", "UNIVERSAL") - `opts`: Optional overrides """ @spec register_bundle_id(String.t(), String.t(), String.t(), opts) :: response def register_bundle_id(identifier, name, platform, opts \\ []) when is_binary(identifier) and is_binary(name) do body = %{ data: %{ type: "bundleIds", attributes: %{ identifier: identifier, name: name, platform: platform } } } Client.post("/v1/bundleIds", body, opts) end @doc """ Delete a bundle ID. ## Parameters - `bundle_id`: The bundle ID resource ID to delete - `opts`: Optional overrides """ @spec delete_bundle_id(String.t(), opts) :: :ok | {:error, term()} def delete_bundle_id(bundle_id, opts \\ []) when is_binary(bundle_id) do case Client.delete("/v1/bundleIds/#{bundle_id}", opts) do {:ok, _} -> :ok error -> error end end # Apps (App Store Connect) @doc """ List all apps in App Store Connect. ## Parameters - `opts`: - `:filter_name` - Filter by app name - `:limit` - Maximum results """ @spec list_apps(opts) :: {:ok, list(map())} | {:error, term()} def list_apps(opts \\ []) do Client.get("/v1/apps", opts) end @doc """ Get a specific app by ID. ## Parameters - `app_id`: The app ID - `opts`: Optional overrides """ @spec get_app(String.t(), opts) :: response def get_app(app_id, opts \\ []) when is_binary(app_id) do Client.get("/v1/apps/#{app_id}", opts) end # Users & Teams @doc """ List all users in your team. ## Parameters - `opts`: - `:filter_roles` - Filter by roles - `:limit` - Maximum results """ @spec list_users(opts) :: {:ok, list(map())} | {:error, term()} def list_users(opts \\ []) do Client.get("/v1/users", opts) end @doc """ Get the current user's info. ## Parameters - `opts`: Optional overrides """ @spec get_user(opts) :: response def get_user(opts \\ []) do Client.get("/v1/user", opts) end end