accessgrid 0.4.0 → 0.6.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 05f4bc4f464d21efa501041653ae1252c64f6d5cca1a29b30394b51ea0cdd486
4
- data.tar.gz: e26bba2fb9ccb37ecfaa5f32676453c9b5a5369b49d11f6cc2385a26345eead6
3
+ metadata.gz: 611a55bbec1d43d8241d9d92601bfcaaf0f40e96e2d53330d56668311bc8dfd4
4
+ data.tar.gz: e9a7fe6162e349464bf93f9cda848e59deb6dc6ff91690be874ec5eab67945e4
5
5
  SHA512:
6
- metadata.gz: 181d498ca94e2514ecf4c5296acf2b4cb5d975efbd77cfa0915280dcc610b7860740cb58ad1d3909f5ca137a30ca610347e10de6d85608c72fbe022e31923eb4
7
- data.tar.gz: d9ccd5c9bf41ba8f68415ea1ad9f72b68b48fa2799f76ce53726cd3fca93466a56e38fe6320b907c57cb67dbb7f8aac06674cd7cba7ac97a31ab44fb32d6ea11
6
+ metadata.gz: 4b1f579ef34c871ee9f8eea7993d8b67c173dfc3b7c173be75be979f6b008fdfaa07bfc8ddac32de84eab163d4e57a6c193f12369c43be52203c2c180062c43a
7
+ data.tar.gz: 16771d5080fcf568017272d1f23accaa168cff849fe5c6b2d03b3d992cd661d5548ad3551d69dd1096a3a1bc3fe9354eeae54c4be513fddd2e8d5c6cf08ce7b9
data/README.md CHANGED
@@ -139,7 +139,7 @@ client.access_cards.delete("0xc4rd1d")
139
139
  template = client.console.create_template(
140
140
  name: "Employee Access Pass",
141
141
  platform: "apple",
142
- use_case: "employee_badge",
142
+ use_case: "corporate_id",
143
143
  protocol: "desfire",
144
144
  allow_on_multiple_devices: true,
145
145
  watch_count: 2,
@@ -185,6 +185,36 @@ template = client.console.update_template(
185
185
  template = client.console.read_template("0xd3adb00b5")
186
186
  ```
187
187
 
188
+ #### Publish a template
189
+
190
+ ```ruby
191
+ result = client.console.publish_template("0xd3adb00b5")
192
+
193
+ puts result.id # "0xd3adb00b5"
194
+ puts result.status # "in-review" (Apple), "ready" (Android), or "publishing" (already in flight)
195
+ ```
196
+
197
+ #### Delete a template
198
+
199
+ ```ruby
200
+ client.console.delete_template("0xd3adb00b5")
201
+ ```
202
+
203
+ #### Reveal a SmartTap private key
204
+
205
+ Fetches the template's SmartTap private key, decrypted client-side. The SDK generates a fresh ephemeral P-256 keypair per call, submits the public half, and decrypts the server's response — you get the plaintext PEM back without touching any crypto.
206
+
207
+ ```ruby
208
+ reveal = client.console.reveal_smart_tap("0xd3adb00b5")
209
+
210
+ puts "Key version: #{reveal.key_version}"
211
+ puts "Collector ID: #{reveal.collector_id}"
212
+ puts "Fingerprint: #{reveal.fingerprint}"
213
+ puts reveal.private_key # PEM — store in your reader/collector key vault
214
+ ```
215
+
216
+ The server enforces single-use on pubkey fingerprint and rate-limits to 1 per minute per account. The SDK uses a fresh keypair every call, so single-use is satisfied automatically. Errors raised by the crypto path (`AccessGrid::DecryptError`, `AccessGrid::InvalidEnvelopeError`) and the HTTP path (`AccessGrid::AuthenticationError`, `AccessGrid::ResourceNotFoundError`, etc.) all descend from `AccessGrid::Error`.
217
+
188
218
  #### Get event logs
189
219
 
190
220
  ```ruby
@@ -294,6 +324,14 @@ end
294
324
  client.console.webhooks.delete('abc123')
295
325
  ```
296
326
 
327
+ #### Verify a webhook
328
+
329
+ ```ruby
330
+ result = client.console.webhooks.verify('abc123')
331
+
332
+ puts "Verified: #{result.verified}"
333
+ ```
334
+
297
335
  ### HID Organizations
298
336
 
299
337
  #### Create an HID org
@@ -404,6 +442,12 @@ puts "Profile created: #{profile.id}"
404
442
  puts "AID: #{profile.aid}"
405
443
  ```
406
444
 
445
+ #### Delete a credential profile
446
+
447
+ ```ruby
448
+ client.console.credential_profiles.delete('a1b2c3d4e5f')
449
+ ```
450
+
407
451
  ## Configuration
408
452
 
409
453
  The SDK can be configured with a custom API endpoint:
@@ -480,15 +524,20 @@ Bug reports and pull requests are welcome on GitHub at https://github.com/access
480
524
  | GET /v1/console/card-template-pairs | `console.list_pass_template_pairs()` | Y |
481
525
  | POST /v1/console/card-template-pairs | `console.create_pass_template_pair()` | Y |
482
526
  | POST /v1/console/card-templates/{id}/ios_preflight | `console.ios_preflight()` | Y |
527
+ | POST /v1/console/card-templates/{id}/publish | `console.publish_template()` | Y |
528
+ | POST /v1/console/card-templates/{id}/smart-tap/reveal | `console.reveal_smart_tap()` | Y |
529
+ | DELETE /v1/console/card-templates/{id} | `console.delete_template()` | Y |
483
530
  | GET /v1/console/ledger-items | `console.list_ledger_items()` / `console.ledger_items()` | Y |
484
531
  | GET /v1/console/webhooks | `console.webhooks.list()` | Y |
485
532
  | POST /v1/console/webhooks | `console.webhooks.create()` | Y |
486
533
  | DELETE /v1/console/webhooks/{id} | `console.webhooks.delete()` | Y |
534
+ | POST /v1/console/webhooks/{id}/verify | `console.webhooks.verify()` | Y |
487
535
  | GET /v1/console/landing-pages | `console.list_landing_pages()` | Y |
488
536
  | POST /v1/console/landing-pages | `console.create_landing_page()` | Y |
489
537
  | PUT /v1/console/landing-pages/{id} | `console.update_landing_page()` | Y |
490
538
  | GET /v1/console/credential-profiles | `console.credential_profiles.list()` | Y |
491
539
  | POST /v1/console/credential-profiles | `console.credential_profiles.create()` | Y |
540
+ | DELETE /v1/console/credential-profiles/{id} | `console.credential_profiles.delete()` | Y |
492
541
  | POST /v1/console/hid/orgs | `console.hid.orgs.create()` | Y |
493
542
  | POST /v1/console/hid/orgs/activate | `console.hid.orgs.activate()` | Y |
494
543
  | GET /v1/console/hid/orgs | `console.hid.orgs.list()` | Y |
@@ -73,6 +73,33 @@ module AccessGrid
73
73
 
74
74
  alias ledger_items list_ledger_items
75
75
 
76
+ def publish_template(template_id)
77
+ response = @client.make_request(:post, "/v1/console/card-templates/#{template_id}/publish")
78
+ PublishTemplateResponse.new(response)
79
+ end
80
+
81
+ def delete_template(template_id)
82
+ @client.make_request(:delete, "/v1/console/card-templates/#{template_id}")
83
+ end
84
+
85
+ # Reveal the SmartTap private key for a card template, decrypted client-side.
86
+ #
87
+ # The SDK generates a fresh ephemeral P-256 keypair per call, submits the
88
+ # public half, and decrypts the server's response. The returned
89
+ # RevealTemplatePrivateKey carries the plaintext PEM in #private_key;
90
+ # the encrypted envelope is consumed internally and not exposed.
91
+ def reveal_smart_tap(template_id)
92
+ keypair = SmartTapRevealCrypto.generate_keypair
93
+ response = @client.make_request(
94
+ :post,
95
+ "/v1/console/card-templates/#{template_id}/smart-tap/reveal",
96
+ { client_public_key: keypair[:pub_pem] }
97
+ )
98
+
99
+ plaintext = SmartTapRevealCrypto.decrypt_envelope(response['encrypted_private_key'], keypair[:priv])
100
+ RevealTemplatePrivateKey.new(response.merge('private_key' => plaintext))
101
+ end
102
+
76
103
  def ios_preflight(card_template_id:, access_pass_ex_id:)
77
104
  data = { access_pass_ex_id: access_pass_ex_id }
78
105
  response = @client.make_request(:post, "/v1/console/card-templates/#{card_template_id}/ios_preflight", data)
@@ -188,6 +215,30 @@ module AccessGrid
188
215
  end
189
216
  end
190
217
 
218
+ # Result of publishing a card template.
219
+ class PublishTemplateResponse
220
+ attr_reader :id, :status
221
+
222
+ def initialize(data)
223
+ @id = data['id']
224
+ @status = data['status']
225
+ end
226
+ end
227
+
228
+ # Result of revealing a SmartTap private key. #private_key is the plaintext
229
+ # PEM, decrypted client-side by the SDK; the encrypted envelope is consumed
230
+ # internally and not exposed.
231
+ class RevealTemplatePrivateKey
232
+ attr_reader :key_version, :collector_id, :fingerprint, :private_key
233
+
234
+ def initialize(data)
235
+ @key_version = data['key_version']
236
+ @collector_id = data['collector_id']
237
+ @fingerprint = data['fingerprint']
238
+ @private_key = data['private_key']
239
+ end
240
+ end
241
+
191
242
  # Represents a billing ledger item.
192
243
  class LedgerItem
193
244
  attr_reader :created_at, :amount, :id, :kind, :metadata, :access_pass
@@ -275,6 +326,10 @@ module AccessGrid
275
326
  profiles = response.is_a?(Array) ? response : response.fetch('credential_profiles', [])
276
327
  profiles.map { |profile| CredentialProfile.new(profile) }
277
328
  end
329
+
330
+ def delete(credential_profile_id)
331
+ @client.make_request(:delete, "/v1/console/credential-profiles/#{credential_profile_id}")
332
+ end
278
333
  end
279
334
 
280
335
  # Manages webhook operations.
@@ -302,6 +357,20 @@ module AccessGrid
302
357
  def delete(webhook_id)
303
358
  @client.make_request(:delete, "/v1/console/webhooks/#{webhook_id}")
304
359
  end
360
+
361
+ def verify(webhook_id)
362
+ response = @client.make_request(:post, "/v1/console/webhooks/#{webhook_id}/verify")
363
+ WebhookVerification.new(response)
364
+ end
365
+ end
366
+
367
+ class WebhookVerification
368
+ attr_reader :id, :verified
369
+
370
+ def initialize(data)
371
+ @id = data['id']
372
+ @verified = data['verified']
373
+ end
305
374
  end
306
375
 
307
376
  # Represents a webhook configuration.
@@ -13,6 +13,15 @@ module AccessGrid
13
13
  # Raised when request parameters fail validation.
14
14
  class ValidationError < Error; end
15
15
 
16
+ # Raised when a SmartTap reveal envelope is missing fields or contains
17
+ # non-base64 / non-PEM data.
18
+ class InvalidEnvelopeError < Error; end
19
+
20
+ # Raised when AES-GCM auth-tag verification fails while decrypting a
21
+ # SmartTap reveal envelope (wrong key, tampered envelope, or wire-format
22
+ # drift between server and SDK).
23
+ class DecryptError < Error; end
24
+
16
25
  # additional error classes to match Python version
17
26
  class AccessGridError < Error; end
18
27
  end
@@ -82,7 +82,8 @@ module AccessGrid
82
82
 
83
83
  last_part = parts.last
84
84
  second_to_last_part = parts[-2]
85
- @resource_id = %w[suspend resume unlink delete].include?(last_part) ? second_to_last_part : last_part
85
+ action_words = %w[suspend resume unlink delete publish verify]
86
+ @resource_id = action_words.include?(last_part) ? second_to_last_part : last_part
86
87
  end
87
88
 
88
89
  def get?
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'openssl'
4
+ require 'base64'
5
+
6
+ module AccessGrid
7
+ # Internal crypto helpers for the SmartTap reveal flow.
8
+ #
9
+ # Driven by Console#reveal_smart_tap; not part of the public SDK surface.
10
+ # Pure stdlib — no new gem deps.
11
+ #
12
+ # @api private
13
+ module SmartTapRevealCrypto
14
+ CURVE = 'prime256v1'
15
+ HKDF_INFO = 'accessgrid-smart-tap-reveal-v1'
16
+ KEY_LEN = 32
17
+
18
+ # Generate a fresh ephemeral P-256 keypair for a reveal call.
19
+ #
20
+ # @return [Hash] `{priv: OpenSSL::PKey::EC, pub_pem: String}`
21
+ def self.generate_keypair
22
+ priv = OpenSSL::PKey::EC.generate(CURVE)
23
+ { priv: priv, pub_pem: priv.public_to_pem }
24
+ end
25
+
26
+ # Decrypt the encrypted_private_key envelope from the reveal endpoint.
27
+ #
28
+ # Performs ECDH(client_priv, server_ephemeral_pub) + HKDF-SHA256 +
29
+ # AES-256-GCM. Must match the server-side encryption parameters exactly.
30
+ #
31
+ # @return [String] the plaintext SmartTap private key PEM.
32
+ # @raise [RuntimeError] on missing/bad envelope or auth-tag verification failure.
33
+ def self.decrypt_envelope(envelope, priv)
34
+ server_pub = parse_ephemeral_public_key(envelope)
35
+ nonce = decode_envelope_bytes(envelope['iv'])
36
+ ciphertext = decode_envelope_bytes(envelope['ciphertext'])
37
+ tag = decode_envelope_bytes(envelope['tag'])
38
+
39
+ aes_key = derive_aes_key(priv, server_pub)
40
+ aes_gcm_decrypt(aes_key, nonce, ciphertext, tag)
41
+ end
42
+
43
+ # @api private
44
+ def self.parse_ephemeral_public_key(envelope)
45
+ pem = envelope['ephemeral_public_key']
46
+ raise InvalidEnvelopeError, 'Invalid ephemeral_public_key in envelope' unless pem.is_a?(String) && !pem.empty?
47
+
48
+ OpenSSL::PKey::EC.new(pem)
49
+ end
50
+
51
+ # @api private
52
+ def self.derive_aes_key(priv, server_pub)
53
+ shared_secret = priv.dh_compute_key(server_pub.public_key)
54
+ OpenSSL::KDF.hkdf(shared_secret, salt: '', info: HKDF_INFO, length: KEY_LEN, hash: 'SHA256')
55
+ end
56
+
57
+ # @api private
58
+ def self.aes_gcm_decrypt(aes_key, nonce, ciphertext, tag)
59
+ cipher = OpenSSL::Cipher.new('aes-256-gcm').decrypt
60
+ cipher.key = aes_key
61
+ cipher.iv = nonce
62
+ cipher.auth_tag = tag
63
+ cipher.auth_data = ''
64
+ cipher.update(ciphertext) + cipher.final
65
+ rescue OpenSSL::Cipher::CipherError
66
+ raise DecryptError, 'AES-GCM decryption failed (auth tag verification)'
67
+ end
68
+
69
+ # @api private
70
+ def self.decode_envelope_bytes(value)
71
+ raise InvalidEnvelopeError, 'Envelope iv/ciphertext/tag must be base64-encoded' unless value.is_a?(String)
72
+
73
+ Base64.strict_decode64(value)
74
+ rescue ArgumentError
75
+ raise InvalidEnvelopeError, 'Envelope iv/ciphertext/tag must be base64-encoded'
76
+ end
77
+ end
78
+ end
@@ -2,5 +2,5 @@
2
2
 
3
3
  # lib/accessgrid/version.rb
4
4
  module AccessGrid
5
- VERSION = '0.4.0'
5
+ VERSION = '0.6.0'
6
6
  end
data/lib/accessgrid.rb CHANGED
@@ -11,6 +11,7 @@ require_relative 'accessgrid/access_cards'
11
11
  require_relative 'accessgrid/console'
12
12
  require_relative 'accessgrid/error'
13
13
  require_relative 'accessgrid/request'
14
+ require_relative 'accessgrid/smart_tap_reveal_crypto'
14
15
  require_relative 'accessgrid/version'
15
16
 
16
17
  # Ruby SDK for the AccessGrid API.
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: accessgrid
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Auston Bunsen
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2026-04-16 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: base64
@@ -38,13 +37,13 @@ files:
38
37
  - lib/accessgrid/console.rb
39
38
  - lib/accessgrid/error.rb
40
39
  - lib/accessgrid/request.rb
40
+ - lib/accessgrid/smart_tap_reveal_crypto.rb
41
41
  - lib/accessgrid/version.rb
42
42
  homepage: https://github.com/access-grid/accessgrid-rb
43
43
  licenses:
44
44
  - MIT
45
45
  metadata:
46
46
  source_code_uri: https://github.com/access-grid/accessgrid-rb
47
- post_install_message:
48
47
  rdoc_options: []
49
48
  require_paths:
50
49
  - lib
@@ -59,8 +58,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
59
58
  - !ruby/object:Gem::Version
60
59
  version: '0'
61
60
  requirements: []
62
- rubygems_version: 3.4.19
63
- signing_key:
61
+ rubygems_version: 4.0.3
64
62
  specification_version: 4
65
63
  summary: AccessGrid API Client
66
64
  test_files: []