app_store_server_api_client 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +13 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +163 -0
- data/Rakefile +12 -0
- data/lib/app_store_server_api/client.rb +155 -0
- data/lib/app_store_server_api/error.rb +108 -0
- data/lib/app_store_server_api/utils/certs/AppleRootCA-G3.cer +0 -0
- data/lib/app_store_server_api/utils/decoder.rb +53 -0
- data/lib/app_store_server_api/utils/http_client.rb +82 -0
- data/lib/app_store_server_api/version.rb +4 -0
- data/lib/app_store_server_api_client.rb +7 -0
- data/lib/app_store_server_api_client.rb~ +7 -0
- data/sig/app_store_server_api_client.rbs +4 -0
- metadata +100 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: e0d6c54805549e9e61a26f3dc874d091722c548ecc57ed45b9086c295f9d02db
|
4
|
+
data.tar.gz: 0f545da8ddc8a22a83d1e9fa382968f45e90a82d9059835822e30d02c2debf28
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: d478dae3696e4f0e8eda24c12f83a9ccdb4f7ffc2e3f5e95c8b0ee62ef664278384fbeba0f728044eab5f1adb8f1eb6ac71a79ae86710d66b59894c06c179f46
|
7
|
+
data.tar.gz: e2106ce7f0313203f26c807f1ee90aa998cce02f4cb7dce765695f8853cdfeb8d82f9d6e3a473f6cd8bcb4cac30709713450cb4dbd04b801b10213dab1cc2c34
|
data/.rspec
ADDED
data/.rubocop.yml
ADDED
data/CHANGELOG.md
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2025 watanabe
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,163 @@
|
|
1
|
+
# App Store Server API Client
|
2
|
+
|
3
|
+
A Ruby client for
|
4
|
+
the [App Store Server API](https://developer.apple.com/documentation/appstoreserverapi).
|
5
|
+
|
6
|
+
## Support API Endpoints
|
7
|
+
|
8
|
+
* [Get Transaction Info](https://developer.apple.com/documentation/appstoreserverapi/get-v1-transactions-_transactionid_)
|
9
|
+
* [Request a Test Notification](https://developer.apple.com/documentation/appstoreserverapi/post-v1-notifications-test)
|
10
|
+
* [Get Test Notification Status](https://developer.apple.com/documentation/appstoreserverapi/get-v1-notifications-test-_testnotificationtoken_)
|
11
|
+
* [Get Transaction History](https://developer.apple.com/documentation/appstoreserverapi/get-v2-history-_transactionid_)
|
12
|
+
|
13
|
+
## Requirements
|
14
|
+
|
15
|
+
Ruby 3.3.0 or later.
|
16
|
+
|
17
|
+
## Installation
|
18
|
+
|
19
|
+
add this line to your application's Gemfile:
|
20
|
+
|
21
|
+
```Gemfile
|
22
|
+
gem 'app_store_server_api_client'
|
23
|
+
```
|
24
|
+
|
25
|
+
## Usage
|
26
|
+
|
27
|
+
### Prerequisites
|
28
|
+
|
29
|
+
To use this, please obtain an API Key.
|
30
|
+
https://developer.apple.com/documentation/appstoreserverapi/creating-api-keys-to-authorize-api-requests
|
31
|
+
|
32
|
+
### Configure
|
33
|
+
|
34
|
+
**In your Rails application, create a client configure**
|
35
|
+
|
36
|
+
```yaml
|
37
|
+
# my_app/config/app_store_server.yml
|
38
|
+
default: &default
|
39
|
+
private_key: |
|
40
|
+
-----BEGIN PRIVATE KEY-----
|
41
|
+
...
|
42
|
+
-----END PRIVATE KEY-----
|
43
|
+
key_id: Z1BT391B21
|
44
|
+
issuer_id: ef02153z-1290-3519-875e-237a15237e3c
|
45
|
+
bundle_id: com.myapp.app
|
46
|
+
environment: sandbox
|
47
|
+
|
48
|
+
development:
|
49
|
+
<<: *default
|
50
|
+
|
51
|
+
test:
|
52
|
+
<<: *default
|
53
|
+
|
54
|
+
production:
|
55
|
+
<<: *default
|
56
|
+
```
|
57
|
+
|
58
|
+
### load the configuration
|
59
|
+
|
60
|
+
```ruby
|
61
|
+
config = Rails.application.config_for(:app_store_server)
|
62
|
+
client = AppStoreServerApi::Client.new(**config)
|
63
|
+
```
|
64
|
+
|
65
|
+
## API
|
66
|
+
|
67
|
+
### Get Transaction Info
|
68
|
+
|
69
|
+
[Get Transaction Info](
|
70
|
+
https://developer.apple.com/documentation/appstoreserverapi/get-v1-transactions-_transactionid_)
|
71
|
+
|
72
|
+
Get information about a single transaction for your app.
|
73
|
+
|
74
|
+
```ruby
|
75
|
+
transaction_id = '2000000847061981'
|
76
|
+
client.get_transaction_info(transaction_id)
|
77
|
+
=>
|
78
|
+
{
|
79
|
+
"transactionId" => "2000000847061981",
|
80
|
+
"originalTransactionId" => "2000000847061981",
|
81
|
+
"bundleId" => "com.myapp.app",
|
82
|
+
"productId" => "com.myapp.app.product",
|
83
|
+
"type" => "Consumable",
|
84
|
+
"purchaseDate" => 1738645560000,
|
85
|
+
"originalPurchaseDate" => 1738645560000,
|
86
|
+
"quantity" => 1,
|
87
|
+
...
|
88
|
+
}
|
89
|
+
```
|
90
|
+
|
91
|
+
### Request a Test Notification
|
92
|
+
|
93
|
+
[Request a Test Notification](https://developer.apple.com/documentation/appstoreserverapi/post-v1-notifications-test)
|
94
|
+
|
95
|
+
Ask App Store Server Notifications to send a test notification to your server.
|
96
|
+
|
97
|
+
```ruby
|
98
|
+
result = client.request_test_notification
|
99
|
+
#=> {"testNotificationToken"=>"9f90efb9-2f75-4dbe-990c-5d1fc89f4546_1739179413123"}
|
100
|
+
```
|
101
|
+
|
102
|
+
### Get Test Notification Status
|
103
|
+
|
104
|
+
[Get Test Notification Status](https://developer.apple.com/documentation/appstoreserverapi/get-v1-notifications-test-_testnotificationtoken_)
|
105
|
+
|
106
|
+
Check the status of the test App Store server notification sent to your server.
|
107
|
+
|
108
|
+
```ruby
|
109
|
+
test_notification_token = client.request_test_notification['testNotificationToken']
|
110
|
+
result = client.get_test_notification_status(test_notification_token)
|
111
|
+
#=> {
|
112
|
+
# "signedPayload"=> "eyJhbGciOiJFUzI1NiIsIng1YyI6...",
|
113
|
+
# "firstSendAttemptResult"=>"SUCCESS",
|
114
|
+
# "sendAttempts"=>[{"attemptDate"=>1739179888814, "sendAttemptResult"=>"SUCCESS"}]
|
115
|
+
#}
|
116
|
+
|
117
|
+
signed_payload = AppStoreServerApi::Utils::Decoder.decode_jws!(result['signedPayload'])
|
118
|
+
# => {
|
119
|
+
# "notificationType"=>"TEST",
|
120
|
+
# "notificationUUID"=>"3838df56-31ab-4e2e-9535-e6e9377c4c77",
|
121
|
+
# "data"=>{"bundleId"=>"com.myapp.app", "environment"=>"Sandbox"},
|
122
|
+
# "version"=>"2.0",
|
123
|
+
# "signedDate"=>1739180480080
|
124
|
+
# }
|
125
|
+
```
|
126
|
+
|
127
|
+
### Get Transaction History
|
128
|
+
|
129
|
+
[Get Transaction History](https://developer.apple.com/documentation/appstoreserverapi/get-v2-history-_transactionid_)
|
130
|
+
|
131
|
+
Get a customer’s in-app purchase transaction history for your app.
|
132
|
+
|
133
|
+
```ruby
|
134
|
+
data = client.get_transaction_history(transaction_id,
|
135
|
+
params: {
|
136
|
+
sort: "DESCENDING"
|
137
|
+
})
|
138
|
+
|
139
|
+
transactions = AppStoreServerApi::Utils::Decoder.decode_transactions(signed_transactions:
|
140
|
+
data["signedTransactions"])
|
141
|
+
```
|
142
|
+
|
143
|
+
## Error Handling
|
144
|
+
|
145
|
+
```ruby
|
146
|
+
|
147
|
+
begin
|
148
|
+
# response success
|
149
|
+
transaction_info = client.get_transaction_info('invalid_transaction_id')
|
150
|
+
rescue AppStoreServerApi::Error => e
|
151
|
+
# response failure
|
152
|
+
# case of error:
|
153
|
+
# - http status 40x, 50x
|
154
|
+
# - json parse error
|
155
|
+
puts e.code # => Integer
|
156
|
+
puts e.message # => String
|
157
|
+
end
|
158
|
+
```
|
159
|
+
|
160
|
+
## License
|
161
|
+
|
162
|
+
The gem is available as open source under the terms of
|
163
|
+
the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
@@ -0,0 +1,155 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'jwt'
|
3
|
+
require 'faraday'
|
4
|
+
require 'uri'
|
5
|
+
require 'json'
|
6
|
+
require 'openssl'
|
7
|
+
|
8
|
+
module AppStoreServerApi
|
9
|
+
class Client
|
10
|
+
attr_reader :environment, :issuer_id, :key_id, :private_key, :bundle_id
|
11
|
+
|
12
|
+
PAYLOAD_AUD = 'appstoreconnect-v1'
|
13
|
+
TOKEN_TYPE = 'JWT'
|
14
|
+
ENCODE_ALGORITHM = 'ES256'
|
15
|
+
ENVIRONMENTS = [:production, :sandbox].freeze
|
16
|
+
API_BASE_URLS = {
|
17
|
+
:production => 'https://api.storekit.itunes.apple.com',
|
18
|
+
:sandbox => 'https://api.storekit-sandbox.itunes.apple.com'
|
19
|
+
}.freeze
|
20
|
+
|
21
|
+
# initialize client
|
22
|
+
# @param private_key [String] p8 key
|
23
|
+
# @param key_id [String] Your private key ID from App Store Connect (Ex: 2X9R4HXF34)
|
24
|
+
# @param issuer_id [String] Your issuer ID from the Keys page in App Store Connect
|
25
|
+
# @param bundle_id [String] Your app’s bundle ID (Ex: “com.example.testbundleid”)
|
26
|
+
# @param environment [Symbol] :production or :sandbox
|
27
|
+
def initialize(private_key:, key_id:, issuer_id:, bundle_id:, environment: :production)
|
28
|
+
self.environment = environment.to_sym
|
29
|
+
@issuer_id = issuer_id
|
30
|
+
@key_id = key_id
|
31
|
+
@private_key = private_key
|
32
|
+
@bundle_id = bundle_id
|
33
|
+
@http_client = Utils::HttpClient.new
|
34
|
+
end
|
35
|
+
|
36
|
+
# set environment
|
37
|
+
# @param env [Symbol] :production or :sandbox
|
38
|
+
# @raise [ArgumentError] if env is not :production or :sandbox
|
39
|
+
def environment=(env)
|
40
|
+
unless ENVIRONMENTS.include?(env)
|
41
|
+
raise ArgumentError, 'environment must be :production or :sandbox'
|
42
|
+
end
|
43
|
+
|
44
|
+
@environment = env
|
45
|
+
end
|
46
|
+
|
47
|
+
# get information about a single transaction
|
48
|
+
# @see https://developer.apple.com/documentation/appstoreserverapi/get-v1-transactions-_transactionid_
|
49
|
+
# @param [String] transaction_id The identifier of a transaction
|
50
|
+
# @return [Hash] transaction info
|
51
|
+
def get_transaction_info(transaction_id)
|
52
|
+
path = "/inApps/v1/transactions/#{transaction_id}"
|
53
|
+
response = do_request(path)
|
54
|
+
json = JSON.parse(response.body)
|
55
|
+
payload, = Utils::Decoder.decode_jws!(json['signedTransactionInfo'])
|
56
|
+
payload
|
57
|
+
end
|
58
|
+
|
59
|
+
# Request a Test Notification
|
60
|
+
# @see https://developer.apple.com/documentation/appstoreserverapi/post-v1-notifications-test
|
61
|
+
# @return [Hash] test notification token info
|
62
|
+
def request_test_notification
|
63
|
+
path = '/inApps/v1/notifications/test'
|
64
|
+
response = do_request(path, method: :post, params: {}.to_json)
|
65
|
+
JSON.parse(response.body)
|
66
|
+
end
|
67
|
+
|
68
|
+
# Get Test Notification Status
|
69
|
+
# @see https://developer.apple.com/documentation/appstoreserverapi/get-v1-notifications-test-_testnotificationtoken_
|
70
|
+
def get_test_notification_status(test_notification_token)
|
71
|
+
path = "/inApps/v1/notifications/test/#{test_notification_token}"
|
72
|
+
response = do_request(path)
|
73
|
+
JSON.parse(response.body)
|
74
|
+
end
|
75
|
+
|
76
|
+
# Get Transaction History
|
77
|
+
# @see https://developer.apple.com/documentation/appstoreserverapi/get-v2-history-_transactionid_
|
78
|
+
# @param [String] transaction_id The identifier of a transaction
|
79
|
+
# @param [Hash] params request params
|
80
|
+
# @return [Hash] transaction history
|
81
|
+
def get_transaction_history(transaction_id, params: {})
|
82
|
+
path = "/inApps/v2/history/#{transaction_id}"
|
83
|
+
response = do_request(path, params: params)
|
84
|
+
JSON.parse(response.body)
|
85
|
+
end
|
86
|
+
|
87
|
+
# generate bearer token
|
88
|
+
# @param issued_at [Time] issued at
|
89
|
+
# @param expired_in [Integer] expired in seconds (max 3600)
|
90
|
+
# @return [String] bearer token
|
91
|
+
def generate_bearer_token(issued_at: Time.now, expired_in: 3600)
|
92
|
+
# expirations longer than 60 minutes will be rejected
|
93
|
+
if expired_in > 3600
|
94
|
+
raise ArgumentError, 'expired_in must be less than or equal to 3600'
|
95
|
+
end
|
96
|
+
|
97
|
+
headers = {
|
98
|
+
alg: ENCODE_ALGORITHM,
|
99
|
+
kid: key_id,
|
100
|
+
typ: TOKEN_TYPE,
|
101
|
+
}
|
102
|
+
|
103
|
+
payload = {
|
104
|
+
iss: issuer_id,
|
105
|
+
iat: issued_at.to_i,
|
106
|
+
exp: (issued_at + expired_in).to_i,
|
107
|
+
aud: PAYLOAD_AUD,
|
108
|
+
bid: bundle_id
|
109
|
+
}
|
110
|
+
|
111
|
+
JWT.encode(payload, OpenSSL::PKey::EC.new(private_key), ENCODE_ALGORITHM, headers)
|
112
|
+
end
|
113
|
+
|
114
|
+
def api_base_url
|
115
|
+
API_BASE_URLS[environment]
|
116
|
+
end
|
117
|
+
|
118
|
+
def base_request_headers(bearer_token)
|
119
|
+
{
|
120
|
+
'Content-Type' => 'application/json',
|
121
|
+
'Authorization' => "Bearer #{bearer_token}"
|
122
|
+
}
|
123
|
+
end
|
124
|
+
|
125
|
+
# send get request to App Store Server API
|
126
|
+
# @param [String] path request path
|
127
|
+
# @param [Symbol] method request method
|
128
|
+
# @param [Hash,String,nil] params request params
|
129
|
+
# @param [Hash] headers additional headers
|
130
|
+
# @return [Faraday::Response] response
|
131
|
+
#
|
132
|
+
# @raise [Error::UnauthorizedError] if unauthorized error
|
133
|
+
# @raise [Error::ServerError] if server error
|
134
|
+
# @raise [Error] if other error
|
135
|
+
def do_request(path, method: :get, params: {}, headers: {}, open_timeout: 10, read_timeout: 30)
|
136
|
+
request_url = api_base_url + path
|
137
|
+
bearer_token = generate_bearer_token
|
138
|
+
request_headers = base_request_headers(bearer_token).merge(headers)
|
139
|
+
|
140
|
+
response = @http_client.request_with_retry(
|
141
|
+
url: request_url,
|
142
|
+
method: method,
|
143
|
+
params: params,
|
144
|
+
headers: request_headers)
|
145
|
+
|
146
|
+
if response.success?
|
147
|
+
return response
|
148
|
+
end
|
149
|
+
|
150
|
+
Error.handle_error(response)
|
151
|
+
end
|
152
|
+
|
153
|
+
end
|
154
|
+
|
155
|
+
end
|
@@ -0,0 +1,108 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module AppStoreServerApi
|
3
|
+
|
4
|
+
class Error < StandardError
|
5
|
+
attr_reader :code, :response
|
6
|
+
|
7
|
+
# initialize error
|
8
|
+
# @param [Integer] code error code
|
9
|
+
# @param [String] message error message
|
10
|
+
# @param [Faraday::Response] response error response
|
11
|
+
def initialize(code:, message:, response:)
|
12
|
+
super(message)
|
13
|
+
@code = code
|
14
|
+
@response = response
|
15
|
+
end
|
16
|
+
|
17
|
+
def to_h
|
18
|
+
{
|
19
|
+
code: code,
|
20
|
+
message: message,
|
21
|
+
response: response
|
22
|
+
}
|
23
|
+
end
|
24
|
+
|
25
|
+
def inspect
|
26
|
+
"#<#{self.class.name}: #{to_h.to_json}>"
|
27
|
+
end
|
28
|
+
|
29
|
+
# The JSON Web Token (JWT) in the authorization header is invalid.
|
30
|
+
# For more information, see Generating JSON Web Tokens for API requests.
|
31
|
+
# @see https://developer.apple.com/documentation/appstoreserverapi/generating-json-web-tokens-for-api-requests
|
32
|
+
# other:
|
33
|
+
# - wrong environment (sandbox/production)
|
34
|
+
class UnauthorizedError < Error
|
35
|
+
def initialize(code: 4010000, message: 'unauthorized error', response:)
|
36
|
+
super(code: code, message: message, response: response)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
class ServerError < Error
|
41
|
+
def initialize(code: 5000000, message: 'Internal Server Error', response:)
|
42
|
+
super(code: code, message: message, response: response)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# error response body is invalid
|
47
|
+
# must have errorCode and errorMessage.
|
48
|
+
# valid example response body:
|
49
|
+
# {
|
50
|
+
# "errorCode": 4000006,
|
51
|
+
# "errorMessage": "Invalid transaction id."
|
52
|
+
# }
|
53
|
+
class InvalidResponseError < Error
|
54
|
+
def initialize(code: 5000002, message: 'response body is invalid', response:)
|
55
|
+
super(code: code, message: message, response: response)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
class TransactionIdNotFoundError < Error; end
|
60
|
+
|
61
|
+
class InvalidTransactionIdError < Error; end
|
62
|
+
|
63
|
+
class RateLimitExceededError < Error; end
|
64
|
+
|
65
|
+
class ServerNotificationURLNotFoundError < Error; end
|
66
|
+
|
67
|
+
class InvalidTestNotificationTokenError < Error; end
|
68
|
+
|
69
|
+
class TestNotificationNotFoundError < Error; end
|
70
|
+
|
71
|
+
# map error code to error class
|
72
|
+
ERROR_CODE_MAP = {
|
73
|
+
4040010 => Error::TransactionIdNotFoundError,
|
74
|
+
4000020 => Error::InvalidTestNotificationTokenError,
|
75
|
+
4000006 => Error::InvalidTransactionIdError,
|
76
|
+
4290000 => Error::RateLimitExceededError,
|
77
|
+
4040007 => Error::ServerNotificationURLNotFoundError,
|
78
|
+
4040008 => Error::TestNotificationNotFoundError,
|
79
|
+
}.freeze
|
80
|
+
|
81
|
+
# raise error from response
|
82
|
+
# @param [Faraday::Response] response error response
|
83
|
+
def self.handle_error(response)
|
84
|
+
case response.status
|
85
|
+
when 401
|
86
|
+
# Unauthorized error
|
87
|
+
# reasons:
|
88
|
+
# - JWT in the authorization header is invalid.
|
89
|
+
raise Error::UnauthorizedError.new(response: response)
|
90
|
+
when 500
|
91
|
+
raise Error::ServerError.new(response: response)
|
92
|
+
else
|
93
|
+
data = JSON.parse(response.body)
|
94
|
+
|
95
|
+
# error object must be {errorCode: Integer, errorMessage: String}
|
96
|
+
unless data.has_key?('errorCode') && data.has_key?('errorMessage')
|
97
|
+
raise Error::InvalidResponseError.new(message: 'response body is invalid', response: response)
|
98
|
+
end
|
99
|
+
|
100
|
+
error_code = data['errorCode']
|
101
|
+
error_class = ERROR_CODE_MAP[error_code] || Error
|
102
|
+
raise error_class.new(code: error_code, message: data['errorMessage'], response: response)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
end
|
107
|
+
|
108
|
+
end
|
Binary file
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'openssl'
|
3
|
+
require 'jwt'
|
4
|
+
|
5
|
+
module AppStoreServerApi
|
6
|
+
module Utils
|
7
|
+
module Decoder
|
8
|
+
module_function
|
9
|
+
|
10
|
+
# Decode a signed JWT
|
11
|
+
# @param [String] jws The signed JWT to decode
|
12
|
+
# @return [Hash] The decoded payload
|
13
|
+
def decode_jws!(jws)
|
14
|
+
apple_cert_store = make_apple_cert_store
|
15
|
+
|
16
|
+
payload, = JWT.decode(jws, nil, true, {algorithm: 'ES256'}) do |headers|
|
17
|
+
# verify the certificate included in the header x5c
|
18
|
+
cert_target, *cert_chain = headers['x5c'].map {|cert| OpenSSL::X509::Certificate.new(Base64.decode64(cert))}
|
19
|
+
apple_cert_store.verify(cert_target, cert_chain)
|
20
|
+
cert_target.public_key
|
21
|
+
end
|
22
|
+
|
23
|
+
payload
|
24
|
+
end
|
25
|
+
|
26
|
+
def decode_transaction(signed_transaction:)
|
27
|
+
decode_jws! signed_transaction
|
28
|
+
end
|
29
|
+
|
30
|
+
def decode_transactions(signed_transactions:)
|
31
|
+
signed_transactions.map do |signed_transaction|
|
32
|
+
decode_transaction signed_transaction: signed_transaction
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def apple_root_certs
|
37
|
+
Dir.glob(File.join(__dir__, 'certs', '*.cer')).map do |filename|
|
38
|
+
OpenSSL::X509::Certificate.new File.read(filename)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def make_apple_cert_store
|
43
|
+
apple_cert_store = OpenSSL::X509::Store.new
|
44
|
+
apple_root_certs.each do |cert|
|
45
|
+
apple_cert_store.add_cert cert
|
46
|
+
end
|
47
|
+
|
48
|
+
apple_cert_store
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'faraday'
|
3
|
+
require 'retriable'
|
4
|
+
|
5
|
+
module AppStoreServerApi
|
6
|
+
|
7
|
+
module Utils
|
8
|
+
|
9
|
+
class HttpClient
|
10
|
+
DEFAULT_OPEN_TIMEOUT = 10
|
11
|
+
DEFAULT_READ_TIMEOUT = 30
|
12
|
+
RETRY_ERRORS = [Faraday::TimeoutError, Faraday::ConnectionFailed, Faraday::ServerError]
|
13
|
+
|
14
|
+
# initialize client
|
15
|
+
# @param open_timeout [Integer] open timeout
|
16
|
+
# @param read_timeout [Integer] read timeout
|
17
|
+
def initialize(open_timeout: DEFAULT_OPEN_TIMEOUT, read_timeout: DEFAULT_READ_TIMEOUT)
|
18
|
+
@open_timeout = open_timeout
|
19
|
+
@read_timeout = read_timeout
|
20
|
+
end
|
21
|
+
|
22
|
+
def build_connection
|
23
|
+
Faraday.new do |f|
|
24
|
+
f.adapter :net_http do |http|
|
25
|
+
http.open_timeout = @open_timeout
|
26
|
+
http.read_timeout = @read_timeout
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def connection
|
32
|
+
@connection ||= build_connection
|
33
|
+
end
|
34
|
+
|
35
|
+
# send request
|
36
|
+
# @param url [String] request url
|
37
|
+
# @param method [Symbol] request method(:get, :post, :put, :patch, :delete)
|
38
|
+
# @param params [Hash,String,nil] request params
|
39
|
+
# @param headers [Hash] request headers
|
40
|
+
# @return [Faraday::Response]
|
41
|
+
def request(url:, method: :get, params: {}, headers: {})
|
42
|
+
method = method.to_sym
|
43
|
+
|
44
|
+
case method
|
45
|
+
when :get, :delete
|
46
|
+
connection.run_request(method, url, nil, headers) do |req|
|
47
|
+
req.params.update(params) if params.is_a?(Hash)
|
48
|
+
end
|
49
|
+
when :post, :put, :patch
|
50
|
+
connection.run_request(method, url, params, headers)
|
51
|
+
else
|
52
|
+
raise ArgumentError, "Unsupported HTTP method: #{method}"
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# send request with retry
|
57
|
+
# @param url [String] request url
|
58
|
+
# @param method [Symbol] request method
|
59
|
+
# @param params [Hash,String,nil] request params
|
60
|
+
# @param headers [Hash] request headers
|
61
|
+
# @param retries [Integer] retry count
|
62
|
+
# @param base_interval [Float] base interval
|
63
|
+
# @param multiplier [Float] multiplier
|
64
|
+
# @param max_interval [Float] max interval
|
65
|
+
# @return [Faraday::Response]
|
66
|
+
def request_with_retry(url:, method: :get, params: {}, headers: {}, retries: 3,
|
67
|
+
base_interval: 0.5, multiplier: 1.0, max_interval: 30)
|
68
|
+
|
69
|
+
Retriable.retriable tries: retries,
|
70
|
+
base_interval: base_interval,
|
71
|
+
max_interval: max_interval,
|
72
|
+
multiplier: multiplier,
|
73
|
+
on: RETRY_ERRORS do
|
74
|
+
request(url: url, method: method, params: params, headers: headers)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
end
|
79
|
+
|
80
|
+
end
|
81
|
+
|
82
|
+
end
|
@@ -0,0 +1,7 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'app_store_server_api/version'
|
4
|
+
require_relative 'app_store_server_api/utils/decoder'
|
5
|
+
require_relative 'app_store_server_api/utils/http_client'
|
6
|
+
require_relative 'app_store_server_api/error'
|
7
|
+
require_relative 'app_store_server_api/client'
|
@@ -0,0 +1,7 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'app_store_server_api/version'
|
4
|
+
require_relative 'app_store_server_api/utils/decoder'
|
5
|
+
require_relative 'app_store_server_api/utils/http_client'
|
6
|
+
require_relative 'app_store_server_api/error'
|
7
|
+
require_relative 'app_store_server_api/client'
|
metadata
ADDED
@@ -0,0 +1,100 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: app_store_server_api_client
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- mingos
|
8
|
+
bindir: exe
|
9
|
+
cert_chain: []
|
10
|
+
date: 2025-02-14 00:00:00.000000000 Z
|
11
|
+
dependencies:
|
12
|
+
- !ruby/object:Gem::Dependency
|
13
|
+
name: jwt
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
15
|
+
requirements:
|
16
|
+
- - "~>"
|
17
|
+
- !ruby/object:Gem::Version
|
18
|
+
version: '2.8'
|
19
|
+
type: :runtime
|
20
|
+
prerelease: false
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
22
|
+
requirements:
|
23
|
+
- - "~>"
|
24
|
+
- !ruby/object:Gem::Version
|
25
|
+
version: '2.8'
|
26
|
+
- !ruby/object:Gem::Dependency
|
27
|
+
name: faraday
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
29
|
+
requirements:
|
30
|
+
- - "~>"
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '2.12'
|
33
|
+
type: :runtime
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - "~>"
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: '2.12'
|
40
|
+
- !ruby/object:Gem::Dependency
|
41
|
+
name: retriable
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
43
|
+
requirements:
|
44
|
+
- - "~>"
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: '3.1'
|
47
|
+
type: :runtime
|
48
|
+
prerelease: false
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - "~>"
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '3.1'
|
54
|
+
description: Manage your customers' App Store transactions from your server.
|
55
|
+
email:
|
56
|
+
- mingos@pumb.jp
|
57
|
+
executables: []
|
58
|
+
extensions: []
|
59
|
+
extra_rdoc_files: []
|
60
|
+
files:
|
61
|
+
- ".rspec"
|
62
|
+
- ".rubocop.yml"
|
63
|
+
- CHANGELOG.md
|
64
|
+
- LICENSE.txt
|
65
|
+
- README.md
|
66
|
+
- Rakefile
|
67
|
+
- lib/app_store_server_api/client.rb
|
68
|
+
- lib/app_store_server_api/error.rb
|
69
|
+
- lib/app_store_server_api/utils/certs/AppleRootCA-G3.cer
|
70
|
+
- lib/app_store_server_api/utils/decoder.rb
|
71
|
+
- lib/app_store_server_api/utils/http_client.rb
|
72
|
+
- lib/app_store_server_api/version.rb
|
73
|
+
- lib/app_store_server_api_client.rb
|
74
|
+
- lib/app_store_server_api_client.rb~
|
75
|
+
- sig/app_store_server_api_client.rbs
|
76
|
+
homepage: https://github.com/mingos/app-store-server-api-client
|
77
|
+
licenses:
|
78
|
+
- MIT
|
79
|
+
metadata:
|
80
|
+
homepage_uri: https://github.com/mingos/app-store-server-api-client
|
81
|
+
source_code_uri: https://github.com/mingos/app-store-server-api-client
|
82
|
+
changelog_uri: https://github.com/mingos/app-store-server-api-client/CHANGELOG.md
|
83
|
+
rdoc_options: []
|
84
|
+
require_paths:
|
85
|
+
- lib
|
86
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
87
|
+
requirements:
|
88
|
+
- - ">="
|
89
|
+
- !ruby/object:Gem::Version
|
90
|
+
version: 3.3.0
|
91
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
92
|
+
requirements:
|
93
|
+
- - ">="
|
94
|
+
- !ruby/object:Gem::Version
|
95
|
+
version: '0'
|
96
|
+
requirements: []
|
97
|
+
rubygems_version: 3.6.2
|
98
|
+
specification_version: 4
|
99
|
+
summary: A Ruby client for App Store Server API
|
100
|
+
test_files: []
|