aho-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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 72d0e9a4889740cb021e3df8a786d25ac7e0005ed8161d77c38cb8de84e8d92a
4
+ data.tar.gz: bce01a4cdb564cca550f17956aead22aa2e56c3e7a862b1ffd5649fbbb3113b0
5
+ SHA512:
6
+ metadata.gz: ab85b572852b0e5ae5247a489685332cd52be3cb2a444cc9b41f9e5c195d678e71aeee995109e074adda5db0f1a383db302ed73ba24e3f1c972a0b6be1bd7c78
7
+ data.tar.gz: 4588cfed2ef06d2edc5c2da651694e70439f4613a19b37fd5690e1f05704102a109b9fc4560f0593d52582289b9b2c88e73b5538b7f492ead25824ecc779cf77
data/CHANGELOG.md ADDED
@@ -0,0 +1,18 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [0.1.0] - 2026-03-27
9
+
10
+ ### Added
11
+
12
+ - Initial release
13
+ - Issuer client for credential issuance
14
+ - Holder client for wallet management
15
+ - Verifier client for presentation verification
16
+ - Account client for account management
17
+ - Pagination support
18
+ - Comprehensive error handling
data/LICENSE.txt ADDED
@@ -0,0 +1,18 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Aho
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
6
+ associated documentation files (the "Software"), to deal in the Software without restriction, including
7
+ without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the
9
+ following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be included in all copies or substantial
12
+ portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
15
+ LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
16
+ EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
17
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
18
+ USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,278 @@
1
+ # AhoSdk Ruby SDK
2
+
3
+ Official Ruby SDK for the [Aho](https://aho.com) Verifiable Credentials API.
4
+
5
+ ## Installation
6
+
7
+ Add to your Gemfile:
8
+
9
+ ```ruby
10
+ gem 'aho-sdk'
11
+ ```
12
+
13
+ Or install directly:
14
+
15
+ ```bash
16
+ gem install aho-sdk
17
+ ```
18
+
19
+ ## Quick Start
20
+
21
+ ```ruby
22
+ require 'aho-sdk'
23
+
24
+ # Initialize the Issuer client
25
+ issuer = AhoSdk::Issuer.new(api_key: ENV['AHO_ISSUER_API_KEY'])
26
+
27
+ # Issue a credential
28
+ credential = issuer.credentials.create(
29
+ schema_uuid: 'your-schema-uuid',
30
+ subject_identifier: 'user@example.com',
31
+ claims: {
32
+ name: 'Jane Doe',
33
+ role: 'Engineer'
34
+ }
35
+ )
36
+
37
+ puts credential['uuid']
38
+ ```
39
+
40
+ ## Clients
41
+
42
+ The SDK provides the following clients:
43
+
44
+ | Client | Purpose | API Key Type |
45
+ |--------|---------|--------------|
46
+ | `AhoSdk::Account` | Manage account settings, domains, and API keys | Account API Key |
47
+ | `AhoSdk::System` | System health and status endpoints | System API Key |
48
+ | `AhoSdk::Holder` | Manage holder credentials and presentations | Holder API Key |
49
+ | `AhoSdk::Verifier` | Create presentation requests and verify credentials | Verifier API Key |
50
+ | `AhoSdk::Issuer` | Issue and manage verifiable credentials | Issuer API Key |
51
+ | `AhoSdk::Schemas` | Browse and retrieve credential schemas | Schemas API Key |
52
+ | `AhoSdk::Public` | Public endpoints (no authentication required) | None (public) |
53
+
54
+ ## Usage Examples
55
+
56
+ ### Issuing Credentials
57
+
58
+ ```ruby
59
+ issuer = AhoSdk::Issuer.new(api_key: ENV['AHO_ISSUER_API_KEY'])
60
+
61
+ # List all schemas
62
+ schemas = issuer.schemas.list
63
+ schemas.data.each { |s| puts s['name'] }
64
+
65
+ # Create a schema
66
+ schema = issuer.schemas.create(
67
+ name: 'EmployeeBadge',
68
+ claims: [
69
+ { name: 'employee_id', type: 'string', required: true },
70
+ { name: 'department', type: 'string', required: true },
71
+ { name: 'hire_date', type: 'date', required: false }
72
+ ]
73
+ )
74
+
75
+ # Issue a credential
76
+ credential = issuer.credentials.create(
77
+ schema_uuid: schema['uuid'],
78
+ subject_identifier: 'jane.doe@company.com',
79
+ claims: {
80
+ employee_id: 'EMP-12345',
81
+ department: 'Engineering',
82
+ hire_date: '2024-01-15'
83
+ }
84
+ )
85
+
86
+ # Revoke a credential
87
+ issuer.credentials.revoke(uuid: credential['uuid'], reason: 'Employee departed')
88
+ ```
89
+
90
+ ### Verifying Credentials
91
+
92
+ ```ruby
93
+ verifier = AhoSdk::Verifier.new(api_key: ENV['AHO_VERIFIER_API_KEY'])
94
+
95
+ # Create a presentation request
96
+ request = verifier.requests.create(
97
+ name: 'Employment Verification',
98
+ query_format: 'dcql',
99
+ credentials: [
100
+ {
101
+ id: 'employee_badge',
102
+ format: 'vc+sd-jwt',
103
+ claims: [
104
+ { path: ['employee_id'] },
105
+ { path: ['department'] }
106
+ ]
107
+ }
108
+ ]
109
+ )
110
+
111
+ # Get the QR code for the request (supports :png, :svg formats)
112
+ qr = verifier.requests.qr_code(uuid: request['uuid'], output_format: :svg)
113
+
114
+ # List responses to the request
115
+ responses = verifier.responses.list(request_uuid: request['uuid'])
116
+ ```
117
+
118
+ ### Managing Holder Credentials
119
+
120
+ ```ruby
121
+ holder = AhoSdk::Holder.new(api_key: ENV['AHO_HOLDER_API_KEY'])
122
+
123
+ # List credentials
124
+ credentials = holder.credentials.list(status: 'active')
125
+
126
+ # Create a presentation (selective disclosure)
127
+ presentation = holder.presentations.create(
128
+ credential_uuid: 'credential-uuid',
129
+ disclosed_claims: ['name', 'department']
130
+ )
131
+ ```
132
+
133
+ ### Account Management
134
+
135
+ ```ruby
136
+ account = AhoSdk::Account.new(api_key: ENV['AHO_API_KEY'])
137
+
138
+ # Manage domains
139
+ domains = account.domains.list
140
+ account.domains.verify(id: domain['id'])
141
+
142
+ # Manage signing keys
143
+ keys = account.signing_keys.list
144
+ account.signing_keys.rotate(id: key['id'])
145
+
146
+ # Configure webhooks
147
+ account.webhooks.create(
148
+ url: 'https://your-app.com/webhooks/aho',
149
+ events: ['credential.issued', 'credential.revoked']
150
+ )
151
+ ```
152
+
153
+ ## Pagination
154
+
155
+ List methods return `Page` objects with built-in iteration:
156
+
157
+ ```ruby
158
+ # Iterate through all pages automatically
159
+ issuer.credentials.list.each do |credential|
160
+ puts credential['uuid']
161
+ end
162
+
163
+ # Or handle pages manually
164
+ page = issuer.credentials.list(per_page: 50)
165
+ while page
166
+ page.data.each { |c| puts c['uuid'] }
167
+ page = page.next_page
168
+ end
169
+
170
+ # Collect all items
171
+ all_credentials = issuer.credentials.list.to_a
172
+ ```
173
+
174
+ ## File Uploads
175
+
176
+ For endpoints that accept file uploads:
177
+
178
+ ```ruby
179
+ # Upload a file
180
+ issuer.media.upload(
181
+ file: File.open('document.pdf'),
182
+ metadata: { description: 'Employee contract' }
183
+ )
184
+
185
+ # The SDK auto-detects MIME types from file extensions
186
+ # You can also pass a path string:
187
+ issuer.media.upload(file: '/path/to/document.pdf')
188
+ ```
189
+
190
+ ## Binary Responses
191
+
192
+ Some endpoints return binary data (images, PDFs):
193
+
194
+ ```ruby
195
+ # Get QR code as PNG
196
+ png_data = verifier.requests.qr_code(uuid: '...', output_format: :png)
197
+ File.binwrite('qr.png', png_data)
198
+
199
+ # Get QR code as SVG
200
+ svg_data = verifier.requests.qr_code(uuid: '...', output_format: :svg)
201
+ File.write('qr.svg', svg_data)
202
+ ```
203
+
204
+ ## Error Handling
205
+
206
+ ```ruby
207
+ begin
208
+ issuer.credentials.create(invalid_params)
209
+ rescue AhoSdk::ValidationError => e
210
+ # 422 - Validation failed
211
+ e.field_errors.each do |error|
212
+ puts "#{error['field']}: #{error['hint']}"
213
+ end
214
+ rescue AhoSdk::AuthenticationError => e
215
+ # 401 - Invalid API key
216
+ puts "Check your API key"
217
+ rescue AhoSdk::NotFoundError => e
218
+ # 404 - Resource not found
219
+ puts "Resource not found: #{e.message}"
220
+ rescue AhoSdk::RateLimitError => e
221
+ # 429 - Rate limited (SDK auto-retries with exponential backoff)
222
+ puts "Retry after #{e.retry_after} seconds"
223
+ rescue AhoSdk::ApiError => e
224
+ # Other API errors
225
+ puts "Error #{e.status_code}: #{e.message}"
226
+ puts "Request ID: #{e.request_id}"
227
+ end
228
+ ```
229
+
230
+ ### Error Classes
231
+
232
+ | Error Class | HTTP Status | Description |
233
+ |-------------|-------------|-------------|
234
+ | `AuthenticationError` | 401 | Invalid or missing API key |
235
+ | `ForbiddenError` | 403 | Insufficient permissions |
236
+ | `NotFoundError` | 404 | Resource not found |
237
+ | `ConflictError` | 409 | Resource conflict |
238
+ | `ValidationError` | 422 | Request validation failed |
239
+ | `RateLimitError` | 429 | Rate limit exceeded |
240
+ | `ServerError` | 5xx | Server-side error |
241
+ | `NetworkError` | - | Connection/timeout error |
242
+ | `ApiError` | * | Base class for all API errors |
243
+
244
+ ## Rate Limiting
245
+
246
+ The SDK automatically handles rate limits with exponential backoff:
247
+
248
+ - **Idempotent methods** (GET, DELETE, PUT): Auto-retry up to 3 times
249
+ - **Non-idempotent methods** (POST, PATCH): Only retry with idempotency key
250
+
251
+ ```ruby
252
+ # Use idempotency keys for safe retries on POST/PATCH
253
+ issuer.credentials.create(
254
+ schema_uuid: '...',
255
+ claims: { ... },
256
+ idempotency_key: 'unique-request-id'
257
+ )
258
+ ```
259
+
260
+ ## Configuration
261
+
262
+ ```ruby
263
+ # Custom configuration
264
+ issuer = AhoSdk::Issuer.new(
265
+ api_key: ENV['AHO_ISSUER_API_KEY'],
266
+ base_url: 'https://api.aho.com', # Custom base URL
267
+ timeout: 60, # Request timeout in seconds
268
+ logger: Logger.new(STDOUT) # Enable debug logging
269
+ )
270
+ ```
271
+
272
+ ## Requirements
273
+
274
+ - Ruby 3.1+
275
+
276
+ ## License
277
+
278
+ MIT
@@ -0,0 +1,226 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Auto-generated by bin/generate_sdks.rb - DO NOT EDIT
4
+
5
+ require_relative "http_client"
6
+ require_relative "page"
7
+ require_relative "cursor_page"
8
+
9
+ module AhoSdk
10
+ # Manage account settings, domains, and API keys
11
+ #
12
+ # @example
13
+ # client = AhoSdk::Account.new(api_key: ENV["AHO_ACCOUNT_API_KEY"])
14
+ # client.api_keys.list
15
+ # client.domains.list
16
+ # client.signing_keys.list
17
+ # client.webhooks.list
18
+ #
19
+ class Account
20
+ # @param api_key [String] API key for authentication
21
+ # @param base_url [String] Base URL (default: https://aho.com)
22
+ # @param timeout [Integer] Request timeout in seconds (default: 30)
23
+ # @param logger [Logger] Optional logger for debugging
24
+ def initialize(api_key:, base_url: "https://aho.com", timeout: 30, logger: nil)
25
+ @client = HttpClient.new(api_key: api_key, base_url: base_url, timeout: timeout, logger: logger)
26
+ @api_keys = ApiKeysResource.new(@client)
27
+ @domains = DomainsResource.new(@client)
28
+ @signing_keys = SigningKeysResource.new(@client)
29
+ @webhooks = WebhooksResource.new(@client)
30
+ end
31
+
32
+ # @return [ApiKeysResource]
33
+ attr_reader :api_keys
34
+ # @return [DomainsResource]
35
+ attr_reader :domains
36
+ # @return [SigningKeysResource]
37
+ attr_reader :signing_keys
38
+ # @return [WebhooksResource]
39
+ attr_reader :webhooks
40
+
41
+
42
+ # Api_keys resource operations
43
+ # @api private
44
+ class ApiKeysResource
45
+ # @api private
46
+ def initialize(client)
47
+ @client = client
48
+ end
49
+
50
+ # List API keys
51
+ #
52
+ # @return [Hash]
53
+ def list
54
+ @client.get("/api/v1/account/api_keys")
55
+ end
56
+
57
+ # Create/regenerate API key
58
+ #
59
+ # @return [Hash]
60
+ def create(body: nil, idempotency_key: nil)
61
+ @client.post("/api/v1/account/api_keys", body: body, idempotency_key: idempotency_key)
62
+ end
63
+
64
+ # Revoke API key
65
+ #
66
+ # @return [Hash]
67
+ def delete(id:)
68
+ @client.delete("/api/v1/account/api_keys/#{id}")
69
+ end
70
+ end
71
+
72
+ # Domains resource operations
73
+ # @api private
74
+ class DomainsResource
75
+ # @api private
76
+ def initialize(client)
77
+ @client = client
78
+ end
79
+
80
+ # Register a domain
81
+ #
82
+ # @return [Hash]
83
+ def create(body: nil, idempotency_key: nil)
84
+ @client.post("/api/v1/account/domains", body: body, idempotency_key: idempotency_key)
85
+ end
86
+
87
+ # List domains
88
+ #
89
+ # @return [Hash]
90
+ def list(txt_status: nil, fully_verified: nil, primary: nil, page: nil, per_page: nil)
91
+ fetch_page = ->(p) {
92
+ response = @client.get("/api/v1/account/domains", params: { txt_status: txt_status, fully_verified: fully_verified, primary: primary, page: p, per_page: per_page })
93
+ Page.new(data: response[:data], meta: response[:meta], fetch_next: fetch_page)
94
+ }
95
+ fetch_page.call(page)
96
+ end
97
+
98
+ # Get domain details
99
+ #
100
+ # @return [Hash]
101
+ def get(id:)
102
+ @client.get("/api/v1/account/domains/#{id}")
103
+ end
104
+
105
+ # Delete a domain
106
+ #
107
+ # @return [Hash]
108
+ def delete(id:)
109
+ @client.delete("/api/v1/account/domains/#{id}")
110
+ end
111
+
112
+ # Verify domain
113
+ #
114
+ # @return [Hash]
115
+ def verify(id:, idempotency_key: nil)
116
+ @client.post("/api/v1/account/domains/#{id}/verify", idempotency_key: idempotency_key)
117
+ end
118
+ end
119
+
120
+ # Signing_keys resource operations
121
+ # @api private
122
+ class SigningKeysResource
123
+ # @api private
124
+ def initialize(client)
125
+ @client = client
126
+ end
127
+
128
+ # Generate a signing key
129
+ #
130
+ # @return [Hash]
131
+ def create(body: nil, idempotency_key: nil)
132
+ @client.post("/api/v1/account/signing_keys", body: body, idempotency_key: idempotency_key)
133
+ end
134
+
135
+ # List signing keys
136
+ #
137
+ # @return [Hash]
138
+ def list(status: nil, algorithm: nil, usable: nil, page: nil, per_page: nil)
139
+ fetch_page = ->(p) {
140
+ response = @client.get("/api/v1/account/signing_keys", params: { status: status, algorithm: algorithm, usable: usable, page: p, per_page: per_page })
141
+ Page.new(data: response[:data], meta: response[:meta], fetch_next: fetch_page)
142
+ }
143
+ fetch_page.call(page)
144
+ end
145
+
146
+ # Get signing key details
147
+ #
148
+ # @return [Hash]
149
+ def get(id:)
150
+ @client.get("/api/v1/account/signing_keys/#{id}")
151
+ end
152
+
153
+ # Rotate a signing key
154
+ #
155
+ # @return [Hash]
156
+ def rotate(id:, body: nil, idempotency_key: nil)
157
+ @client.post("/api/v1/account/signing_keys/#{id}/rotate", body: body, idempotency_key: idempotency_key)
158
+ end
159
+
160
+ # Revoke a signing key
161
+ #
162
+ # @return [Hash]
163
+ def revoke(id:, idempotency_key: nil)
164
+ @client.post("/api/v1/account/signing_keys/#{id}/revoke", idempotency_key: idempotency_key)
165
+ end
166
+
167
+ # Download X.509 certificate
168
+ #
169
+ # @return [Hash]
170
+ def certificate(id:, domain: nil)
171
+ @client.get("/api/v1/account/signing_keys/#{id}/certificate", params: { domain: domain })
172
+ end
173
+ end
174
+
175
+ # Webhooks resource operations
176
+ # @api private
177
+ class WebhooksResource
178
+ # @api private
179
+ def initialize(client)
180
+ @client = client
181
+ end
182
+
183
+ # List webhooks
184
+ #
185
+ # @return [Hash]
186
+ def list
187
+ @client.get("/api/v1/account/webhooks")
188
+ end
189
+
190
+ # Create webhook
191
+ #
192
+ # @return [Hash]
193
+ def create(body: nil, idempotency_key: nil)
194
+ @client.post("/api/v1/account/webhooks", body: body, idempotency_key: idempotency_key)
195
+ end
196
+
197
+ # Get webhook details
198
+ #
199
+ # @return [Hash]
200
+ def get
201
+ @client.get("/api/v1/account/webhooks/primary")
202
+ end
203
+
204
+ # Update webhook
205
+ #
206
+ # @return [Hash]
207
+ def update(body: nil, idempotency_key: nil)
208
+ @client.patch("/api/v1/account/webhooks/primary", body: body, idempotency_key: idempotency_key)
209
+ end
210
+
211
+ # Delete webhook
212
+ #
213
+ # @return [Hash]
214
+ def delete
215
+ @client.delete("/api/v1/account/webhooks/primary")
216
+ end
217
+
218
+ # Test webhook
219
+ #
220
+ # @return [Hash]
221
+ def test(idempotency_key: nil)
222
+ @client.post("/api/v1/account/webhooks/primary/test", idempotency_key: idempotency_key)
223
+ end
224
+ end
225
+ end
226
+ end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Auto-generated by bin/generate_sdks.rb - DO NOT EDIT
4
+
5
+ module AhoSdk
6
+ # Cursor-paginated response with lazy iteration support
7
+ #
8
+ # @example Iterate through all pages
9
+ # page = issuer.credentials.list
10
+ # page.each do |credential|
11
+ # puts credential[:uuid]
12
+ # end
13
+ #
14
+ # @example Manual pagination
15
+ # page = issuer.credentials.list
16
+ # while page
17
+ # page.data.each { |c| puts c[:uuid] }
18
+ # page = page.next_page
19
+ # end
20
+ #
21
+ class CursorPage
22
+ include Enumerable
23
+
24
+ attr_reader :data, :meta
25
+
26
+ # @param data [Array<Hash>] Items on this page
27
+ # @param meta [Hash] Pagination metadata (cursor, has_more, next_cursor)
28
+ # @param fetch_next [Proc] Lambda to fetch the next page given a cursor
29
+ def initialize(data:, meta:, fetch_next:)
30
+ @data = data
31
+ @meta = meta
32
+ @fetch_next = fetch_next
33
+ end
34
+
35
+ # Iterate through all items across all pages (lazy loading)
36
+ # @yield [Hash] Each item
37
+ # @return [Enumerator] if no block given
38
+ def each(&block)
39
+ return enum_for(:each) unless block_given?
40
+
41
+ page = self
42
+ loop do
43
+ page.data.each(&block)
44
+ page = page.next_page
45
+ break if page.nil?
46
+ end
47
+ end
48
+
49
+ # Fetch the next page
50
+ # @return [CursorPage, nil] Next page or nil if this is the last page
51
+ def next_page
52
+ return nil unless has_more?
53
+
54
+ cursor = next_cursor
55
+ return nil if cursor.nil?
56
+
57
+ @fetch_next.call(cursor)
58
+ end
59
+
60
+ # @return [Boolean] true if there are more pages
61
+ def has_more?
62
+ meta[:has_more] || meta["has_more"] || false
63
+ end
64
+
65
+ # @return [String, nil] Cursor for the next page
66
+ def next_cursor
67
+ meta[:next_cursor] || meta["next_cursor"]
68
+ end
69
+
70
+ # @return [String, nil] Cursor for the current page (if provided)
71
+ def cursor
72
+ meta[:cursor] || meta["cursor"]
73
+ end
74
+
75
+ # @return [Integer] Number of items per page (if provided)
76
+ def per_page
77
+ meta[:per_page] || meta["per_page"]
78
+ end
79
+
80
+ # @return [Integer] Number of items on this page
81
+ def size
82
+ data.size
83
+ end
84
+
85
+ alias_method :length, :size
86
+ end
87
+ end