anyoauth-sdk 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.
Files changed (6) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +51 -0
  4. data/lib/anyoauth.rb +156 -0
  5. data/schema.json +347 -0
  6. metadata +46 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 28c09676d41c9d6b86f8599043828ee352ffa54ba48354402f0f63abf65dec1a
4
+ data.tar.gz: 88a3a0290ec255ebe55aeadee5ed6781238b38a1ac2b907cb95916f7046d5444
5
+ SHA512:
6
+ metadata.gz: b7d28499e334796ebd40116be9c1c184acf638aaee9b5a75cb57b8342c1fe9feb6db0d43f11aee5dee0ff5ca3d7349ee807fb48d0aeb327d025a952217cc5524
7
+ data.tar.gz: 20fe8772718f39fce34398c97f8127cea1fe86b9c85f2780de08fba131fc21ee4f21487ad81f70b1a652b3b5dbe80448efacc31f1163df6c40ba17c2e641ac40
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) AnyOAuth contributors
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 all
13
+ 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 THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,51 @@
1
+ # anyoauth-sdk
2
+
3
+ Generated AnyOAuth ruby confidential-backend SDK, version 0.1.0. MIT licensed. Model descriptors are generated from OpenAPI and embedded in the runtime (also published in schema.json). Objects use JSON field names; optional additive response fields are preserved.
4
+
5
+ ## Install / build
6
+
7
+ Build with `gem build anyoauth-sdk.gemspec`, then install the resulting gem. Require `anyoauth`.
8
+
9
+ From this SDK repository root:
10
+
11
+ ```sh
12
+ ruby -c lib/anyoauth.rb
13
+ ruby -c tests/conformance.rb
14
+ python3 tests/selftest.py
15
+ ruby tests/conformance.rb < input.json
16
+ ```
17
+
18
+ The runner reads one JSON object and prints one JSON result. Example input: `{"action":"pkceChallenge","params":{"codeVerifier":"dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"}}`. Tests use a local HTTP server and dummy credentials only.
19
+
20
+ ## Public API
21
+
22
+ Construct `AnyOAuth::Client.new(client_id, client_secret, base_url)`. The origin defaults to https://api.anyoauth.com. Each operation accepts a parameter hash with string keys.
23
+
24
+ - `exchange_code`
25
+ - `profile`
26
+ - `revoke`
27
+ - `providers`
28
+ - `pkce_challenge`
29
+ - `authorization_url`
30
+ - `validate_callback`
31
+ - `create_transaction`
32
+
33
+ Canonical parameters: exchangeCode {code, codeVerifier, redirectUri}; profile/revoke {accessToken}; pkceChallenge {codeVerifier}; authorizationUrl {provider, redirectUri, state, codeChallenge}; validateCallback {callbackUrl, redirectUri, state, expiresAt}; createTransaction {}. Revocation returns null/nil. Errors expose status, code and safe message. Default timeout is 10 seconds with no redirects or retries.
34
+
35
+ Persist the transaction in the initiating browser's server session, binding it to the exact registered callback. Construct the authorization URL locally, then navigate the browser. Validate the callback and consume the saved transaction atomically (including provider denial). Exchange once with the original verifier, retrieve profile, identify by subject rather than email, and create your application's session. Nullable profile fields are legitimate. Keep secrets on the backend; never put tokens or the verifier in URLs. Helpers do not manage session storage.
36
+
37
+ Generated reproducibly by `node sdks/generate.mjs --target=ruby`; edit sdks/templates/server-dynamic.mjs rather than generated sources.
38
+
39
+ ## Standalone conformance
40
+
41
+ Run `node tests/check.mjs` from this package after installing/building dependencies. The shared suite exercises all eight public actions through the native runtime, schema failures, wire formats, redirects, no retries, and deadlines. Node is a test harness dependency only.
42
+
43
+ ## Source and releases
44
+
45
+ Repository: https://github.com/AnyOAuth/anyoauth-ruby
46
+
47
+ GitHub prerelease: https://github.com/AnyOAuth/anyoauth-ruby/releases/tag/v0.1.0
48
+
49
+ Clone this repository, then run `node verify.mjs` with the native toolchain installed to build and execute the package's conformance suite. Source is generated in the AnyOAuth monorepo; propose changes there so every language stays aligned. GitHub hosts source archives and checksums. Package registry publication is separate.
50
+
51
+ This initial source release has full advertised-method conformance coverage. Runtime line/branch coverage for the whole language matrix remains a separate release gate; see https://anyoauth.com/docs/libraries/.
data/lib/anyoauth.rb ADDED
@@ -0,0 +1,156 @@
1
+ require 'json'
2
+ require 'uri'
3
+ require 'net/http'
4
+ require 'openssl'
5
+ require 'securerandom'
6
+ require 'base64'
7
+ require 'digest'
8
+
9
+ module AnyOAuth
10
+ DESCRIPTORS = JSON.parse('{"schemas":{"Provider":{"type":"string","enum":["google","github"]},"RedirectUri":{"type":"string","format":"uri","maxLength":2048,"x-validation":"redirect","description":"Exact registered HTTPS URL or HTTP loopback; no credentials, query or fragment."},"TokenRequest":{"type":"object","additionalProperties":false,"required":["grant_type","client_id","client_secret","code","redirect_uri","code_verifier"],"properties":{"grant_type":{"type":"string","const":"authorization_code"},"client_id":{"type":"string","maxLength":100},"client_secret":{"type":"string","minLength":1,"maxLength":200},"code":{"type":"string","minLength":1,"maxLength":200},"redirect_uri":{"$ref":"#/components/schemas/RedirectUri"},"code_verifier":{"type":"string","pattern":"^[A-Za-z0-9._~-]{43,128}$"}}},"TokenResponse":{"type":"object","required":["access_token","token_type","expires_in","scope"],"properties":{"access_token":{"type":"string","pattern":"^aop_[A-Za-z0-9_-]{43}$"},"token_type":{"type":"string","const":"Bearer"},"expires_in":{"type":"integer","const":900},"scope":{"type":"string","const":"profile"}}},"Profile":{"type":"object","required":["subject","provider","providerSubject","name","username","email","emailVerified","avatarUrl"],"properties":{"subject":{"type":"string"},"provider":{"$ref":"#/components/schemas/Provider"},"providerSubject":{"type":"string"},"name":{"type":["string","null"]},"username":{"type":["string","null"]},"email":{"type":["string","null"]},"emailVerified":{"type":"boolean"},"avatarUrl":{"type":["string","null"]}}},"ProviderStatus":{"type":"object","required":["id","configured","enabled","available"],"properties":{"id":{"$ref":"#/components/schemas/Provider"},"configured":{"type":"boolean"},"enabled":{"type":"boolean"},"available":{"type":"boolean"}}},"ProviderStatuses":{"type":"array","items":{"$ref":"#/components/schemas/ProviderStatus"}},"ApiError":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"message":{"type":"string"}}},"Transaction":{"type":"object","required":["state","codeVerifier","codeChallenge","createdAt","expiresAt"],"properties":{"state":{"type":"string","pattern":"^[A-Za-z0-9_-]{43}$"},"codeVerifier":{"type":"string","pattern":"^[A-Za-z0-9._~-]{43,128}$"},"codeChallenge":{"type":"string","pattern":"^[A-Za-z0-9_-]{43}$"},"createdAt":{"type":"integer"},"expiresAt":{"type":"integer"}}}},"operations":{"exchangeCode":{"id":"exchangeCode","path":"/v1/token","method":"POST","auth":"client","requestSchema":"TokenRequest","responseSchema":"TokenResponse","successStatus":200,"inputs":{"grant_type":{"constant":"authorization_code"},"client_id":{"param":"clientId"},"client_secret":{"param":"clientSecret"},"code":{"param":"code"},"redirect_uri":{"param":"redirectUri"},"code_verifier":{"param":"codeVerifier"}}},"profile":{"id":"profile","path":"/v1/profile","method":"GET","auth":"bearer","requestSchema":null,"responseSchema":"Profile","successStatus":200,"inputs":{}},"revoke":{"id":"revoke","path":"/v1/revoke","method":"POST","auth":"bearer","requestSchema":null,"responseSchema":null,"successStatus":204,"inputs":{}},"providers":{"id":"providers","path":"/api/providers","method":"GET","auth":"none","requestSchema":null,"responseSchema":"ProviderStatuses","successStatus":200,"inputs":{}}},"authorization":{"path":"/authorize","parameters":[{"name":"client_id","in":"query","required":true,"schema":{"type":"string","minLength":1}},{"name":"provider","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Provider"}},{"name":"redirect_uri","in":"query","required":true,"schema":{"$ref":"#/components/schemas/RedirectUri"}},{"name":"state","in":"query","required":true,"schema":{"type":"string","minLength":16,"maxLength":512}},{"name":"code_challenge","in":"query","required":true,"schema":{"type":"string","pattern":"^[A-Za-z0-9_-]{43}$"}},{"name":"code_challenge_method","in":"query","required":true,"schema":{"type":"string","const":"S256"}},{"name":"response_type","in":"query","required":true,"schema":{"type":"string","const":"code"}}]},"transaction":{"lifetimeSeconds":600,"entropyBytes":32,"description":"Create random state and a PKCE verifier/challenge. Persist this transaction in the initiating browser or app session.","input":{"type":"object","properties":{},"additionalProperties":false}},"defaultOrigin":"https://api.anyoauth.com"}')
11
+ SCHEMAS = DESCRIPTORS.fetch('schemas')
12
+ class Error < StandardError
13
+ attr_reader :status, :code
14
+ def initialize(status = 0, code = 'invalid_argument', message = 'Invalid input')
15
+ @status, @code = status, code
16
+ super(message)
17
+ end
18
+ end
19
+ class Client
20
+ def initialize(client_id = '', client_secret = '', base_url = DESCRIPTORS['defaultOrigin'])
21
+ @client_id, @client_secret, @base_url = client_id, client_secret, url(base_url, true)
22
+ end
23
+ def url(value, origin = false)
24
+ raise Error.new unless value.is_a?(String) && !value.match?(/[\s\\]/)
25
+ u = URI.parse(value)
26
+ raise Error.new unless u.host && !u.userinfo && !value.include?('?') && !value.include?('#') && !value.split('://',2).last.split(/[\/?#]/).first.include?('@')
27
+ raise Error.new unless u.scheme == 'https' || (u.scheme == 'http' && ['localhost','127.0.0.1','[::1]'].include?(u.host))
28
+ raise Error.new if origin && !['','/'].include?(u.path)
29
+ origin ? value.sub(%r{/+$}, '') : value
30
+ rescue URI::InvalidURIError, URI::InvalidComponentError
31
+ raise Error.new
32
+ end
33
+ def validate(value, schema)
34
+ return validate(value, SCHEMAS.fetch(schema['$ref'].split('/').last)) if schema['$ref']
35
+ types = Array(schema['type'])
36
+ matches = {'null'=>value.nil?, 'string'=>value.is_a?(String), 'integer'=>value.is_a?(Integer) || (value.is_a?(Float) && value.finite? && value == value.to_i), 'boolean'=>[true,false].include?(value), 'object'=>value.is_a?(Hash), 'array'=>value.is_a?(Array)}
37
+ raise Error.new if !types.empty? && !types.any? { |t| matches[t] }
38
+ raise Error.new if schema.key?('const') && value != schema['const']
39
+ raise Error.new if schema['enum'] && !schema['enum'].include?(value)
40
+ if value.is_a?(String)
41
+ raise Error.new if value.length < schema.fetch('minLength',0) || value.length > schema.fetch('maxLength',Float::INFINITY)
42
+ raise Error.new if schema['pattern'] && !Regexp.new(schema['pattern'].sub(/\$$/, '\\z')).match?(value)
43
+ url(value) if schema['x-validation'] == 'redirect'
44
+ end
45
+ if value.is_a?(Hash)
46
+ raise Error.new unless Array(schema['required']).all? { |k| value.key?(k) }
47
+ props = schema.fetch('properties', {})
48
+ raise Error.new if schema['additionalProperties'] == false && (value.keys-props.keys).any?
49
+ props.each { |k,s| validate(value[k], s) if value.key?(k) }
50
+ end
51
+ value.each { |item| validate(item, schema['items']) } if value.is_a?(Array) && schema['items']
52
+ value
53
+ end
54
+ def request(action, params)
55
+ op = DESCRIPTORS['operations'].fetch(action)
56
+ headers = {'Accept'=>'application/json', 'User-Agent'=>'AnyOAuth-ruby/0.1.0'}
57
+ if op['auth'] == 'bearer'
58
+ token = params['accessToken']
59
+ raise Error.new unless token.is_a?(String) && token.match?(%r{\A[A-Za-z0-9._~+/-]+=*\z})
60
+ headers['Authorization'] = 'Bearer '+token
61
+ end
62
+ body = nil
63
+ if op['requestSchema']
64
+ values = params.merge('clientId'=>@client_id, 'clientSecret'=>@client_secret)
65
+ data = op['inputs'].to_h { |k,v| [k, v.key?('constant') ? v['constant'] : values[v['param']]] }
66
+ validate(data, SCHEMAS[op['requestSchema']])
67
+ body = JSON.generate(data)
68
+ headers['Content-Type'] = 'application/json'
69
+ end
70
+ begin
71
+ uri = URI.parse(@base_url + op['path'])
72
+ http = Net::HTTP.new(uri.host, uri.port, nil)
73
+ http.use_ssl = uri.scheme == 'https'
74
+ http.verify_mode = OpenSSL::SSL::VERIFY_PEER
75
+ http.open_timeout = http.read_timeout = http.write_timeout = 10
76
+ http.max_retries = 0
77
+ req = Net::HTTPGenericRequest.new(op['method'], !body.nil?, true, uri.request_uri, headers)
78
+ req.body = body unless body.nil?
79
+ response = http.request(req)
80
+ rescue StandardError
81
+ raise Error.new(0, 'transport_error', 'HTTP request failed')
82
+ end
83
+ status = response.code.to_i
84
+ if status != op['successStatus']
85
+ raise Error.new(status, 'protocol_error', 'Unexpected response status') if status >= 200 && status < 300
86
+ code, message = 'protocol_error', 'Invalid error response'
87
+ begin
88
+ error = validate(JSON.parse(response.body), SCHEMAS['ApiError'])
89
+ code, message = error['error'], error.fetch('message', 'HTTP request failed')
90
+ rescue StandardError
91
+ # Infrastructure errors may be non-JSON; never expose raw response bytes.
92
+ end
93
+ raise Error.new(status, code, message)
94
+ end
95
+ return nil unless op['responseSchema']
96
+ begin
97
+ validate(JSON.parse(response.body), SCHEMAS[op['responseSchema']])
98
+ rescue StandardError
99
+ raise Error.new(status, 'protocol_error', 'Invalid API response')
100
+ end
101
+ end
102
+ def exchange_code(params = {})
103
+ request('exchangeCode', params)
104
+ end
105
+ def profile(params = {})
106
+ request('profile', params)
107
+ end
108
+ def revoke(params = {})
109
+ request('revoke', params)
110
+ end
111
+ def providers(params = {})
112
+ request('providers', params)
113
+ end
114
+ def pkce_challenge(params)
115
+ v = params['codeVerifier']
116
+ raise Error.new unless v.is_a?(String) && v.match?(/\A[A-Za-z0-9._~-]{43,128}\z/)
117
+ Base64.urlsafe_encode64(Digest::SHA256.digest(v), padding: false)
118
+ end
119
+ def create_transaction(_params = {})
120
+ settings = DESCRIPTORS['transaction']
121
+ state, verifier = 2.times.map { SecureRandom.urlsafe_base64(settings['entropyBytes'], false) }
122
+ now = Time.now.to_i
123
+ validate({'state'=>state,'codeVerifier'=>verifier,'codeChallenge'=>pkce_challenge('codeVerifier'=>verifier),'createdAt'=>now,'expiresAt'=>now+settings['lifetimeSeconds']}, SCHEMAS['Transaction'])
124
+ end
125
+ def authorization_url(params)
126
+ values = params.merge('clientId'=>@client_id)
127
+ query = DESCRIPTORS['authorization']['parameters'].to_h do |p|
128
+ s = p['schema']; key = p['name'].gsub(/_([a-z])/) { $1.upcase }
129
+ [p['name'], validate(s.key?('const') ? s['const'] : values[key], s)]
130
+ end
131
+ @base_url + DESCRIPTORS['authorization']['path'] + '?' + URI.encode_www_form(query)
132
+ end
133
+ def validate_callback(params)
134
+ redirect = url(params['redirectUri'])
135
+ callback, state, expiry = params.values_at('callbackUrl','state','expiresAt')
136
+ raise Error.new unless state.is_a?(String) && !state.empty? && expiry.is_a?(Integer) && expiry > Time.now.to_i
137
+ raise Error.new unless callback.is_a?(String) && !callback.match?(/[\s\\#]/) && callback.split('?',2)[0] == redirect
138
+ query = callback.split('?',2)[1] || ''
139
+ raise Error.new if query.match?(/%(?![0-9a-fA-F]{2})/)
140
+ pairs = URI.decode_www_form(query).group_by(&:first).transform_values { |p| p.map(&:last) }
141
+ raise Error.new if ['state','code','error'].any? { |k| pairs.fetch(k,[]).length > 1 }
142
+ actual = pairs.fetch('state',[''])[0]
143
+ # Compare fixed-size digests without an early exit, including on older OpenSSL gems.
144
+ left, right = [actual,state].map { |s| Digest::SHA256.digest(s).bytes }
145
+ difference = left.zip(right).reduce(0) { |bits,(a,b)| bits | (a ^ b) }
146
+ raise Error.new unless difference == 0
147
+ raise Error.new if pairs.key?('code') == pairs.key?('error')
148
+ key = pairs.key?('code') ? 'code' : 'error'
149
+ raise Error.new if pairs[key][0].empty?
150
+ {key=>pairs[key][0], 'state'=>actual}
151
+ rescue ArgumentError
152
+ raise Error.new
153
+ end
154
+ private :url, :request
155
+ end
156
+ end
data/schema.json ADDED
@@ -0,0 +1,347 @@
1
+ {
2
+ "schemas": {
3
+ "Provider": {
4
+ "type": "string",
5
+ "enum": [
6
+ "google",
7
+ "github"
8
+ ]
9
+ },
10
+ "RedirectUri": {
11
+ "type": "string",
12
+ "format": "uri",
13
+ "maxLength": 2048,
14
+ "x-validation": "redirect",
15
+ "description": "Exact registered HTTPS URL or HTTP loopback; no credentials, query or fragment."
16
+ },
17
+ "TokenRequest": {
18
+ "type": "object",
19
+ "additionalProperties": false,
20
+ "required": [
21
+ "grant_type",
22
+ "client_id",
23
+ "client_secret",
24
+ "code",
25
+ "redirect_uri",
26
+ "code_verifier"
27
+ ],
28
+ "properties": {
29
+ "grant_type": {
30
+ "type": "string",
31
+ "const": "authorization_code"
32
+ },
33
+ "client_id": {
34
+ "type": "string",
35
+ "maxLength": 100
36
+ },
37
+ "client_secret": {
38
+ "type": "string",
39
+ "minLength": 1,
40
+ "maxLength": 200
41
+ },
42
+ "code": {
43
+ "type": "string",
44
+ "minLength": 1,
45
+ "maxLength": 200
46
+ },
47
+ "redirect_uri": {
48
+ "$ref": "#/components/schemas/RedirectUri"
49
+ },
50
+ "code_verifier": {
51
+ "type": "string",
52
+ "pattern": "^[A-Za-z0-9._~-]{43,128}$"
53
+ }
54
+ }
55
+ },
56
+ "TokenResponse": {
57
+ "type": "object",
58
+ "required": [
59
+ "access_token",
60
+ "token_type",
61
+ "expires_in",
62
+ "scope"
63
+ ],
64
+ "properties": {
65
+ "access_token": {
66
+ "type": "string",
67
+ "pattern": "^aop_[A-Za-z0-9_-]{43}$"
68
+ },
69
+ "token_type": {
70
+ "type": "string",
71
+ "const": "Bearer"
72
+ },
73
+ "expires_in": {
74
+ "type": "integer",
75
+ "const": 900
76
+ },
77
+ "scope": {
78
+ "type": "string",
79
+ "const": "profile"
80
+ }
81
+ }
82
+ },
83
+ "Profile": {
84
+ "type": "object",
85
+ "required": [
86
+ "subject",
87
+ "provider",
88
+ "providerSubject",
89
+ "name",
90
+ "username",
91
+ "email",
92
+ "emailVerified",
93
+ "avatarUrl"
94
+ ],
95
+ "properties": {
96
+ "subject": {
97
+ "type": "string"
98
+ },
99
+ "provider": {
100
+ "$ref": "#/components/schemas/Provider"
101
+ },
102
+ "providerSubject": {
103
+ "type": "string"
104
+ },
105
+ "name": {
106
+ "type": [
107
+ "string",
108
+ "null"
109
+ ]
110
+ },
111
+ "username": {
112
+ "type": [
113
+ "string",
114
+ "null"
115
+ ]
116
+ },
117
+ "email": {
118
+ "type": [
119
+ "string",
120
+ "null"
121
+ ]
122
+ },
123
+ "emailVerified": {
124
+ "type": "boolean"
125
+ },
126
+ "avatarUrl": {
127
+ "type": [
128
+ "string",
129
+ "null"
130
+ ]
131
+ }
132
+ }
133
+ },
134
+ "ProviderStatus": {
135
+ "type": "object",
136
+ "required": [
137
+ "id",
138
+ "configured",
139
+ "enabled",
140
+ "available"
141
+ ],
142
+ "properties": {
143
+ "id": {
144
+ "$ref": "#/components/schemas/Provider"
145
+ },
146
+ "configured": {
147
+ "type": "boolean"
148
+ },
149
+ "enabled": {
150
+ "type": "boolean"
151
+ },
152
+ "available": {
153
+ "type": "boolean"
154
+ }
155
+ }
156
+ },
157
+ "ProviderStatuses": {
158
+ "type": "array",
159
+ "items": {
160
+ "$ref": "#/components/schemas/ProviderStatus"
161
+ }
162
+ },
163
+ "ApiError": {
164
+ "type": "object",
165
+ "required": [
166
+ "error"
167
+ ],
168
+ "properties": {
169
+ "error": {
170
+ "type": "string"
171
+ },
172
+ "message": {
173
+ "type": "string"
174
+ }
175
+ }
176
+ },
177
+ "Transaction": {
178
+ "type": "object",
179
+ "required": [
180
+ "state",
181
+ "codeVerifier",
182
+ "codeChallenge",
183
+ "createdAt",
184
+ "expiresAt"
185
+ ],
186
+ "properties": {
187
+ "state": {
188
+ "type": "string",
189
+ "pattern": "^[A-Za-z0-9_-]{43}$"
190
+ },
191
+ "codeVerifier": {
192
+ "type": "string",
193
+ "pattern": "^[A-Za-z0-9._~-]{43,128}$"
194
+ },
195
+ "codeChallenge": {
196
+ "type": "string",
197
+ "pattern": "^[A-Za-z0-9_-]{43}$"
198
+ },
199
+ "createdAt": {
200
+ "type": "integer"
201
+ },
202
+ "expiresAt": {
203
+ "type": "integer"
204
+ }
205
+ }
206
+ }
207
+ },
208
+ "operations": {
209
+ "exchangeCode": {
210
+ "id": "exchangeCode",
211
+ "path": "/v1/token",
212
+ "method": "POST",
213
+ "auth": "client",
214
+ "requestSchema": "TokenRequest",
215
+ "responseSchema": "TokenResponse",
216
+ "successStatus": 200,
217
+ "inputs": {
218
+ "grant_type": {
219
+ "constant": "authorization_code"
220
+ },
221
+ "client_id": {
222
+ "param": "clientId"
223
+ },
224
+ "client_secret": {
225
+ "param": "clientSecret"
226
+ },
227
+ "code": {
228
+ "param": "code"
229
+ },
230
+ "redirect_uri": {
231
+ "param": "redirectUri"
232
+ },
233
+ "code_verifier": {
234
+ "param": "codeVerifier"
235
+ }
236
+ }
237
+ },
238
+ "profile": {
239
+ "id": "profile",
240
+ "path": "/v1/profile",
241
+ "method": "GET",
242
+ "auth": "bearer",
243
+ "requestSchema": null,
244
+ "responseSchema": "Profile",
245
+ "successStatus": 200,
246
+ "inputs": {}
247
+ },
248
+ "revoke": {
249
+ "id": "revoke",
250
+ "path": "/v1/revoke",
251
+ "method": "POST",
252
+ "auth": "bearer",
253
+ "requestSchema": null,
254
+ "responseSchema": null,
255
+ "successStatus": 204,
256
+ "inputs": {}
257
+ },
258
+ "providers": {
259
+ "id": "providers",
260
+ "path": "/api/providers",
261
+ "method": "GET",
262
+ "auth": "none",
263
+ "requestSchema": null,
264
+ "responseSchema": "ProviderStatuses",
265
+ "successStatus": 200,
266
+ "inputs": {}
267
+ }
268
+ },
269
+ "authorization": {
270
+ "path": "/authorize",
271
+ "parameters": [
272
+ {
273
+ "name": "client_id",
274
+ "in": "query",
275
+ "required": true,
276
+ "schema": {
277
+ "type": "string",
278
+ "minLength": 1
279
+ }
280
+ },
281
+ {
282
+ "name": "provider",
283
+ "in": "query",
284
+ "required": true,
285
+ "schema": {
286
+ "$ref": "#/components/schemas/Provider"
287
+ }
288
+ },
289
+ {
290
+ "name": "redirect_uri",
291
+ "in": "query",
292
+ "required": true,
293
+ "schema": {
294
+ "$ref": "#/components/schemas/RedirectUri"
295
+ }
296
+ },
297
+ {
298
+ "name": "state",
299
+ "in": "query",
300
+ "required": true,
301
+ "schema": {
302
+ "type": "string",
303
+ "minLength": 16,
304
+ "maxLength": 512
305
+ }
306
+ },
307
+ {
308
+ "name": "code_challenge",
309
+ "in": "query",
310
+ "required": true,
311
+ "schema": {
312
+ "type": "string",
313
+ "pattern": "^[A-Za-z0-9_-]{43}$"
314
+ }
315
+ },
316
+ {
317
+ "name": "code_challenge_method",
318
+ "in": "query",
319
+ "required": true,
320
+ "schema": {
321
+ "type": "string",
322
+ "const": "S256"
323
+ }
324
+ },
325
+ {
326
+ "name": "response_type",
327
+ "in": "query",
328
+ "required": true,
329
+ "schema": {
330
+ "type": "string",
331
+ "const": "code"
332
+ }
333
+ }
334
+ ]
335
+ },
336
+ "transaction": {
337
+ "lifetimeSeconds": 600,
338
+ "entropyBytes": 32,
339
+ "description": "Create random state and a PKCE verifier/challenge. Persist this transaction in the initiating browser or app session.",
340
+ "input": {
341
+ "type": "object",
342
+ "properties": {},
343
+ "additionalProperties": false
344
+ }
345
+ },
346
+ "defaultOrigin": "https://api.anyoauth.com"
347
+ }
metadata ADDED
@@ -0,0 +1,46 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: anyoauth-sdk
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - AnyOAuth contributors
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2026-10-07 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description:
14
+ email:
15
+ executables: []
16
+ extensions: []
17
+ extra_rdoc_files: []
18
+ files:
19
+ - LICENSE
20
+ - README.md
21
+ - lib/anyoauth.rb
22
+ - schema.json
23
+ homepage:
24
+ licenses:
25
+ - MIT
26
+ metadata: {}
27
+ post_install_message:
28
+ rdoc_options: []
29
+ require_paths:
30
+ - lib
31
+ required_ruby_version: !ruby/object:Gem::Requirement
32
+ requirements:
33
+ - - ">="
34
+ - !ruby/object:Gem::Version
35
+ version: '2.6'
36
+ required_rubygems_version: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ requirements: []
42
+ rubygems_version: 3.0.3.1
43
+ signing_key:
44
+ specification_version: 4
45
+ summary: AnyOAuth confidential backend SDK
46
+ test_files: []