├── src ├── Exceptions │ ├── ConfigException.php │ ├── Exception.php │ ├── RuntimeException.php │ └── InvalidArgumentException.php ├── Api │ ├── ApiInterface.php │ ├── Certificates.php │ ├── Device.php │ ├── BundleIdCapability.php │ ├── BundleId.php │ ├── Profiles.php │ └── AbstractApi.php ├── Utils │ ├── HttpClient.php │ └── JWT.php └── Client.php ├── composer.json └── README.md /src/Exceptions/ConfigException.php: -------------------------------------------------------------------------------- 1 | get('/certificates', $params); 11 | } 12 | } -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "yunchuang/appstore-connect-api", 3 | "description": "sdk for appstore connect api", 4 | "type": "library", 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "zengbin", 9 | "email": "zengb02@mingyuanyun.com" 10 | } 11 | ], 12 | "minimum-stability": "stable", 13 | "require": { 14 | "rmccue/requests": "^1.7", 15 | "php-curl-class/php-curl-class": "^8.6" 16 | }, 17 | "autoload": { 18 | "psr-4": { 19 | "MingYuanYun\\AppStore\\": "src/" 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Api/Device.php: -------------------------------------------------------------------------------- 1 | get('/devices', $params); 12 | } 13 | 14 | public function register($name, $platform, $udid) 15 | { 16 | $data = [ 17 | 'data' => [ 18 | 'type' => 'devices', 19 | 'attributes' => [ 20 | 'name' => $name, 21 | 'platform' => strtoupper($platform), 22 | 'udid' => $udid, 23 | ] 24 | ] 25 | ]; 26 | return $this->postJson('/devices', $data); 27 | } 28 | } -------------------------------------------------------------------------------- /src/Api/BundleIdCapability.php: -------------------------------------------------------------------------------- 1 | [ 13 | 'type' => 'bundleIdCapabilities', 14 | 'relationships' => [ 15 | 'bundleId' => [ 16 | 'data' => [ 17 | 'type' => 'bundleIds', 18 | 'id' => $bId 19 | ] 20 | ], 21 | ], 22 | 'attributes' => [ 23 | 'capabilityType' => $capability 24 | ], 25 | ] 26 | ]; 27 | 28 | return $this->postJson('/bundleIdCapabilities', $data); 29 | } 30 | 31 | public function disable($bcId) 32 | { 33 | return $this->delete('/bundleIdCapabilities/' . $bcId); 34 | } 35 | } -------------------------------------------------------------------------------- /src/Api/BundleId.php: -------------------------------------------------------------------------------- 1 | get('/bundleIds', $params); 12 | } 13 | 14 | public function register($name, $platform, $bundleId) 15 | { 16 | $data = [ 17 | 'data' => [ 18 | 'type' => 'bundleIds', 19 | 'attributes' => [ 20 | 'identifier' => $bundleId, 21 | 'name' => $name, 22 | 'platform' => $platform 23 | ] 24 | ] 25 | ]; 26 | return $this->postJson('/bundleIds', $data); 27 | } 28 | 29 | public function drop($bId) 30 | { 31 | return $this->delete('/bundleIds/' . $bId); 32 | } 33 | 34 | public function query($bId, array $params = []) 35 | { 36 | return $this->get("/bundleIds/{$bId}/bundleIdCapabilities", $params); 37 | } 38 | } -------------------------------------------------------------------------------- /src/Utils/HttpClient.php: -------------------------------------------------------------------------------- 1 | curl = new Curl(); 18 | return $this; 19 | } 20 | 21 | public function get($url, array $params = [], array $headers = []) 22 | { 23 | $this->getCurl(); 24 | foreach ($headers as $key => $value) { 25 | $this->curl->setHeader($key, $value); 26 | } 27 | $this->curl->get($url, $params); 28 | return $this->wrapContent($this->curl->getResponse()); 29 | } 30 | 31 | public function postJson($url, array $body = [], array $headers = []) 32 | { 33 | $this->getCurl(); 34 | foreach ($headers as $key => $value) { 35 | $this->curl->setHeader($key, $value); 36 | } 37 | $this->curl->setHeader('Content-Type', 'application/json'); 38 | $this->curl->post($url, $body); 39 | return $this->wrapContent($this->curl->getResponse()); 40 | } 41 | 42 | public function delete($url, array $params = [], array $headers = []) 43 | { 44 | $this->getCurl(); 45 | foreach ($headers as $key => $value) { 46 | $this->curl->setHeader($key, $value); 47 | } 48 | $this->curl->delete($url, $params); 49 | return $this->wrapContent($this->curl->getResponse()); 50 | } 51 | 52 | protected function wrapContent($content) 53 | { 54 | if (is_string($content)) { 55 | $content = json_decode(implode('', explode(PHP_EOL, $content))); 56 | } 57 | return json_decode(json_encode($content), true); 58 | } 59 | } -------------------------------------------------------------------------------- /src/Api/Profiles.php: -------------------------------------------------------------------------------- 1 | get('/profiles', $params); 12 | } 13 | 14 | public function create($name, $bId, $profileType, array $devices = [], array $certificates = []) 15 | { 16 | $data = [ 17 | 'data' => [ 18 | 'type' => 'profiles', 19 | 'relationships' => [ 20 | 'bundleId' => [ 21 | 'data' => [ 22 | 'type' => 'bundleIds', 23 | 'id' => $bId 24 | ], 25 | ], 26 | 'devices' => [ 27 | 'data' => [] 28 | ], 29 | 'certificates' => [ 30 | 'data' => [] 31 | ], 32 | ], 33 | 'attributes' => [ 34 | 'profileType' => $profileType, 35 | 'name' => $name 36 | ] 37 | ] 38 | ]; 39 | foreach ($devices as $device) { 40 | $data['data']['relationships']['devices']['data'][] = [ 41 | 'type' => 'devices', 42 | 'id' => $device 43 | ]; 44 | } 45 | foreach ($certificates as $certificate) { 46 | $data['data']['relationships']['certificates']['data'][] = [ 47 | 'type' => 'certificates', 48 | 'id' => $certificate 49 | ]; 50 | } 51 | return $this->postJson('/profiles', $data); 52 | } 53 | 54 | public function listDevices($pId, array $params = []) 55 | { 56 | return $this->get('/profiles/' . $pId . '/devices', $params); 57 | } 58 | 59 | public function listCertificates($pId, array $params = []) 60 | { 61 | return $this->get('/profiles/' . $pId . '/relationships/certificates', $params); 62 | } 63 | 64 | public function drop($pId) 65 | { 66 | return $this->delete('/profiles/' . $pId); 67 | } 68 | } -------------------------------------------------------------------------------- /src/Api/AbstractApi.php: -------------------------------------------------------------------------------- 1 | client = $client; 19 | } 20 | 21 | public function getPage() 22 | { 23 | return $this->page; 24 | } 25 | 26 | public function setPage($page) 27 | { 28 | $this->page = (null === $page ? $page : (int) $page); 29 | 30 | return $this; 31 | } 32 | 33 | public function getPerPage() 34 | { 35 | return $this->perPage; 36 | } 37 | 38 | public function setPerPage($perPage) 39 | { 40 | $this->perPage = (null === $perPage ? $perPage : (int) $perPage); 41 | 42 | return $this; 43 | } 44 | 45 | protected function get($path, array $parameters = [], array $requestHeaders = []) 46 | { 47 | if (null !== $this->page && !isset($parameters['page'])) { 48 | $parameters['page'] = $this->page; 49 | } 50 | if (null !== $this->perPage && !isset($parameters['limit'])) { 51 | $parameters['limit'] = $this->perPage; 52 | } 53 | if (array_key_exists('ref', $parameters) && null === $parameters['ref']) { 54 | unset($parameters['ref']); 55 | } 56 | 57 | $url = $this->client->buildBaseUrl() . $path; 58 | $this->mergeHeaders($requestHeaders); 59 | 60 | return $this->client->get($url, $parameters, $this->client->getHeaders()); 61 | } 62 | 63 | protected function postJson($path, array $parameters = [], array $requestHeaders = []) 64 | { 65 | $url = $this->client->buildBaseUrl() . $path; 66 | $this->mergeHeaders($requestHeaders); 67 | return $this->client->postJson($url, $parameters, $this->client->getHeaders()); 68 | } 69 | 70 | protected function delete($path, array $parameters = [], array $requestHeaders = []) 71 | { 72 | $url = $this->client->buildBaseUrl() . $path; 73 | $this->mergeHeaders($requestHeaders); 74 | return $this->client->delete($url, $parameters, $this->client->getHeaders()); 75 | } 76 | 77 | private function mergeHeaders(array $headers = []) 78 | { 79 | if ($headers) { 80 | $this->client->setHeaders($headers); 81 | } 82 | $this->client->checkAuthHeader(); 83 | } 84 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # appstore connect api 2 | 3 | > unoffical sdk for appstore connect api. *currently partially* 4 | 5 | [see detail](https://developer.apple.com/documentation/appstoreconnectapi) 6 | 7 | 8 | ## install 9 | 10 | ``` 11 | composer require yunchuang/appstore-connect-api 12 | ``` 13 | 14 | ## example 15 | ```php 16 | 17 | use MingYuanYun\AppStore\Client; 18 | 19 | $config = [ 20 | 'iss' => 'xx-xx-xx-xx-xxx', 21 | 'kid' => 'xx', 22 | 'secret' => '/path/to/private.p8' 23 | ]; 24 | 25 | $client = new Client($config); 26 | 27 | // get jwt auth token, expired after 20 minutes later 28 | $token = $client->getToken(); 29 | 30 | // set request auth header 31 | $headers = [ 32 | 'Authorization' => 'Bearer ' . $token, 33 | ]; 34 | $client->setHeaders($headers); 35 | 36 | 37 | // query devices 38 | $queryParams = [ 39 | 'filter[platform]' => 'IOS', 40 | 'filter[status]' => 'ENABLED', 41 | 'filter[udid]' => '9be78daa0dbc12f3a95442caa164f36ab0b1ba47', 42 | 'limit' => 1 43 | ]; 44 | $devices = $client->api('device')->all($queryParams); 45 | 46 | 47 | // add device 48 | $deviceName = 'test'; 49 | $platform = 'IOS'; 50 | $deviceUdid = '9be78daa0dbc12f3a95442caa164f36ab0b1ba47'; 51 | $result = $client->api('device')->register($deviceName, $platform, $deviceUdid); 52 | 53 | 54 | // query bundleId 55 | $params = [ 56 | 'fields[bundleIds]' => 'identifier', 57 | 'filter[identifier]' => 'com.xx.xxx' 58 | ]; 59 | $result = $client->api('bundleId')->all($params); 60 | 61 | 62 | // register bundleId 63 | $name = 'test'; 64 | $platform = 'IOS'; 65 | $bundleId = 'com.xx.test'; 66 | $result = $client->api('bundleId')->register($name, $platform, $bundleId); 67 | 68 | 69 | // delete bundleId 70 | $id = 'xx'; 71 | $result = $client->api('bundleId')->drop($id); 72 | 73 | 74 | // query capabilities of the bundleId 75 | $bid = 'xx'; 76 | $params = [ 77 | 'fields[bundleIdCapabilities]' => 'capabilityType' 78 | ]; 79 | $result = $client->api('bundleId')->query($bid); 80 | 81 | 82 | // add capability for the bundleId 83 | $bid = 'xx'; 84 | $capability = 'PUSH_NOTIFICATIONS'; 85 | $result = $client->api('bundleIdCapabilities')->enable($bid, $capability); 86 | 87 | // query profile 88 | $params = [ 89 | 'filter[id]' => 'xx', 90 | 'fields[profiles]' => 'bundleId,createdDate,expirationDate,name,profileState,profileType,uuid,profileContent' 91 | ]; 92 | $result = $client->api('profiles')->query($params); 93 | 94 | // create profile for the bundleId 95 | $bId = 'xx'; 96 | $name = 'mdev3'; 97 | $profileType = 'IOS_APP_DEVELOPMENT'; 98 | $devices = [ 99 | 'xx1', 'xx2', 'xx3' 100 | ]; 101 | $certificates = [ 102 | 'xx1' 103 | ]; 104 | $result = $client->api('profiles')->create($name, $bId, $profileType, $devices, $certificates); 105 | ``` 106 | 107 | 108 | ## remark 109 | 110 | - the profile content is base64 encoded, so you should base64 decode firstly, and then save as xxx.mobileprovision. 111 | -------------------------------------------------------------------------------- /src/Utils/JWT.php: -------------------------------------------------------------------------------- 1 | payload = json_encode($payload); 20 | $this->header = json_encode($header); 21 | $this->secret = $secret; 22 | } 23 | 24 | public static function encode(array $payload, array $header, string $secret) 25 | { 26 | $jwt = new JWT($payload, $header, $secret); 27 | return $jwt->create(); 28 | } 29 | 30 | public function create() 31 | { 32 | $header = $this->prepare($this->header); 33 | 34 | $claims = $this->prepare($this->payload); 35 | 36 | $signature = $this->prepare( 37 | $this->sign("$header.$claims") 38 | ); 39 | 40 | return $header . '.' . $claims . '.' . $signature; 41 | } 42 | 43 | protected function sign($data) 44 | { 45 | if (!openssl_sign($data, $signature, $this->secret, OPENSSL_ALGO_SHA256)) { 46 | throw new RuntimeException('openssl加密失败'); 47 | } 48 | 49 | return static::fromDER($signature, 64); 50 | } 51 | 52 | private function prepare($data) 53 | { 54 | return str_replace(['+', '/', '='], ['-', '_', ''], base64_encode($data)); 55 | } 56 | 57 | private static function fromDER(string $der, int $partLength): string 58 | { 59 | $hex = \unpack('H*', $der)[1]; 60 | if ('30' !== \mb_substr($hex, 0, 2, '8bit')) { // SEQUENCE 61 | throw new RuntimeException(); 62 | } 63 | if ('81' === \mb_substr($hex, 2, 2, '8bit')) { // LENGTH > 128 64 | $hex = \mb_substr($hex, 6, null, '8bit'); 65 | } else { 66 | $hex = \mb_substr($hex, 4, null, '8bit'); 67 | } 68 | if ('02' !== \mb_substr($hex, 0, 2, '8bit')) { // INTEGER 69 | throw new RuntimeException(); 70 | } 71 | $Rl = \hexdec(\mb_substr($hex, 2, 2, '8bit')); 72 | $R = static::retrievePositiveInteger(\mb_substr($hex, 4, $Rl * 2, '8bit')); 73 | $R = \str_pad($R, $partLength, '0', STR_PAD_LEFT); 74 | $hex = \mb_substr($hex, 4 + $Rl * 2, null, '8bit'); 75 | if ('02' !== \mb_substr($hex, 0, 2, '8bit')) { // INTEGER 76 | throw new RuntimeException(); 77 | } 78 | $Sl = \hexdec(\mb_substr($hex, 2, 2, '8bit')); 79 | $S = static::retrievePositiveInteger(\mb_substr($hex, 4, $Sl * 2, '8bit')); 80 | $S = \str_pad($S, $partLength, '0', STR_PAD_LEFT); 81 | return \pack('H*', $R.$S); 82 | } 83 | 84 | private static function retrievePositiveInteger(string $data): string 85 | { 86 | while ('00' === \mb_substr($data, 0, 2, '8bit') && \mb_substr($data, 2, 2, '8bit') > '7f') { 87 | $data = \mb_substr($data, 2, null, '8bit'); 88 | } 89 | return $data; 90 | } 91 | } -------------------------------------------------------------------------------- /src/Client.php: -------------------------------------------------------------------------------- 1 | config = $config; 38 | $this->iss = !array_key_exists('iss', $config) ?: $config['iss']; 39 | $this->kid = !array_key_exists('kid', $config) ?: $config['kid']; 40 | $secret = !array_key_exists('secret', $config) ?: $config['secret']; 41 | $this->setSecret($secret); 42 | $this->apiVersion = !array_key_exists('apiVersion', $config) ? 'v1' : $config['apiVersion']; 43 | } 44 | 45 | public function getIss() 46 | { 47 | return $this->iss; 48 | } 49 | 50 | public function setIss($iss) 51 | { 52 | $this->iss = $iss; 53 | } 54 | 55 | public function getKid() 56 | { 57 | return $this->kid; 58 | } 59 | 60 | public function setKid($kid) 61 | { 62 | $this->kid = $kid; 63 | } 64 | 65 | public function getSecret() 66 | { 67 | return $this->secret; 68 | } 69 | 70 | public function setSecret($secret) 71 | { 72 | if (file_exists($secret)) { 73 | $this->secret = file_get_contents($secret); 74 | } else { 75 | $this->secret = $secret; 76 | } 77 | } 78 | 79 | public function setToken($jwtToken) 80 | { 81 | $this->headers['Authorization'] = 'Bearer ' . $jwtToken; 82 | } 83 | 84 | public function getToken() 85 | { 86 | if (!$this->iss || !$this->kid || !$this->secret) { 87 | throw new ConfigException('缺少必要的配置'); 88 | } 89 | return $this->generateJwt(); 90 | } 91 | 92 | public function checkAuthHeader() 93 | { 94 | if (!\array_key_exists('Authorization', $this->headers)) { 95 | $jwtToken = $this->getToken(); 96 | $this->headers['Authorization'] = 'Bearer ' . $jwtToken; 97 | } 98 | } 99 | 100 | public function setHeaders(array $headers) 101 | { 102 | if (!$this->headers) { 103 | $this->headers = []; 104 | } 105 | $this->headers = array_merge($this->headers, $headers); 106 | } 107 | 108 | public function getHeaders() 109 | { 110 | return $this->headers; 111 | } 112 | 113 | protected function generateJwt() 114 | { 115 | $payload = $this->buildPayload(); 116 | $header = $this->buildHeader(); 117 | $secret = $this->secret; 118 | 119 | return JWT::encode($payload, $header, $secret); 120 | } 121 | 122 | private function buildPayload() 123 | { 124 | return [ 125 | 'iss' => $this->getIss(), 126 | 'exp' => time() + 20 * 60, 127 | 'aud' => static::JWT_AUD 128 | ]; 129 | } 130 | 131 | private function buildHeader() 132 | { 133 | return [ 134 | 'kid' => $this->getKid(), 135 | 'alg' => static::JWT_ALG, 136 | 'typ' => 'JWT' 137 | ]; 138 | } 139 | 140 | public function buildBaseUrl() 141 | { 142 | return sprintf('https://%s/%s', static::BASE_URI, $this->apiVersion); 143 | } 144 | 145 | public function api($name) 146 | { 147 | switch ($name) { 148 | case 'device': 149 | $api = new Api\Device($this); 150 | break; 151 | case 'bundleId': 152 | $api = new Api\BundleId($this); 153 | break; 154 | case 'bundleIdCapabilities': 155 | $api = new Api\BundleIdCapability($this); 156 | break; 157 | case 'profiles': 158 | $api = new Api\Profiles($this); 159 | break; 160 | case 'certificates': 161 | $api = new Api\Certificates($this); 162 | break; 163 | default: 164 | throw new InvalidArgumentException('未定义的接口'); 165 | } 166 | 167 | return $api; 168 | } 169 | } --------------------------------------------------------------------------------