didit-sdk 1.0.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/CHANGELOG.md +25 -0
- data/LICENSE +21 -0
- data/README.md +277 -0
- data/lib/didit/authentication/base64_url.rb +34 -0
- data/lib/didit/authentication/cache.rb +201 -0
- data/lib/didit/authentication/http.rb +150 -0
- data/lib/didit/authentication/issuer.rb +115 -0
- data/lib/didit/authentication/jws.rb +231 -0
- data/lib/didit/authentication/key_set.rb +89 -0
- data/lib/didit/authentication/pkce.rb +64 -0
- data/lib/didit/authentication/types.rb +175 -0
- data/lib/didit/authentication.rb +546 -0
- data/lib/didit/client.rb +211 -0
- data/lib/didit/errors.rb +153 -0
- data/lib/didit/file_part.rb +85 -0
- data/lib/didit/internal/config.rb +94 -0
- data/lib/didit/internal/errors.rb +83 -0
- data/lib/didit/internal/log.rb +41 -0
- data/lib/didit/internal/multipart.rb +60 -0
- data/lib/didit/internal/request.rb +322 -0
- data/lib/didit/internal/transport.rb +198 -0
- data/lib/didit/internal/util.rb +76 -0
- data/lib/didit/internal.rb +15 -0
- data/lib/didit/model.rb +199 -0
- data/lib/didit/page.rb +130 -0
- data/lib/didit/raw_response.rb +57 -0
- data/lib/didit/resource.rb +15 -0
- data/lib/didit/resources/account.rb +309 -0
- data/lib/didit/resources/address_validation.rb +73 -0
- data/lib/didit/resources/age_estimation.rb +110 -0
- data/lib/didit/resources/aml.rb +166 -0
- data/lib/didit/resources/aml_monitoring.rb +30 -0
- data/lib/didit/resources/analytics.rb +87 -0
- data/lib/didit/resources/bank.rb +65 -0
- data/lib/didit/resources/billing.rb +61 -0
- data/lib/didit/resources/biometric_templates.rb +234 -0
- data/lib/didit/resources/blocklist.rb +130 -0
- data/lib/didit/resources/business_sessions.rb +29 -0
- data/lib/didit/resources/businesses.rb +471 -0
- data/lib/didit/resources/cases.rb +2116 -0
- data/lib/didit/resources/customization.rb +153 -0
- data/lib/didit/resources/database_validation.rb +327 -0
- data/lib/didit/resources/document_ai.rb +145 -0
- data/lib/didit/resources/email.rb +254 -0
- data/lib/didit/resources/face_match.rb +103 -0
- data/lib/didit/resources/face_search.rb +103 -0
- data/lib/didit/resources/fraud_checks.rb +130 -0
- data/lib/didit/resources/id_verification.rb +163 -0
- data/lib/didit/resources/id_wallets.rb +1468 -0
- data/lib/didit/resources/investigator.rb +419 -0
- data/lib/didit/resources/ip.rb +68 -0
- data/lib/didit/resources/kyb.rb +256 -0
- data/lib/didit/resources/lists.rb +332 -0
- data/lib/didit/resources/monitoring_events.rb +466 -0
- data/lib/didit/resources/networks.rb +427 -0
- data/lib/didit/resources/passive_liveness.rb +103 -0
- data/lib/didit/resources/phone.rb +271 -0
- data/lib/didit/resources/pricing.rb +40 -0
- data/lib/didit/resources/proof_of_address.rb +167 -0
- data/lib/didit/resources/questionnaires.rb +170 -0
- data/lib/didit/resources/regulatory_reports.rb +314 -0
- data/lib/didit/resources/report_templates.rb +146 -0
- data/lib/didit/resources/sessions.rb +2820 -0
- data/lib/didit/resources/transactions.rb +981 -0
- data/lib/didit/resources/travel_rule.rb +1202 -0
- data/lib/didit/resources/users.rb +521 -0
- data/lib/didit/resources/wallet_screening.rb +190 -0
- data/lib/didit/resources/webhooks.rb +396 -0
- data/lib/didit/resources/workflows.rb +214 -0
- data/lib/didit/resources.rb +263 -0
- data/lib/didit/types/account.rb +11 -0
- data/lib/didit/types/account_components.rb +105 -0
- data/lib/didit/types/address_validation.rb +8 -0
- data/lib/didit/types/age_estimation.rb +89 -0
- data/lib/didit/types/aml.rb +193 -0
- data/lib/didit/types/aml_monitoring.rb +73 -0
- data/lib/didit/types/analytics.rb +62 -0
- data/lib/didit/types/bank.rb +8 -0
- data/lib/didit/types/billing.rb +18 -0
- data/lib/didit/types/biometric_templates.rb +292 -0
- data/lib/didit/types/blocklist.rb +31 -0
- data/lib/didit/types/businesses.rb +45 -0
- data/lib/didit/types/cases.rb +368 -0
- data/lib/didit/types/components.rb +12206 -0
- data/lib/didit/types/customization.rb +8 -0
- data/lib/didit/types/database_validation.rb +177 -0
- data/lib/didit/types/document_ai.rb +135 -0
- data/lib/didit/types/email.rb +116 -0
- data/lib/didit/types/face_match.rb +93 -0
- data/lib/didit/types/face_search.rb +129 -0
- data/lib/didit/types/fraud_checks.rb +8 -0
- data/lib/didit/types/id_verification.rb +231 -0
- data/lib/didit/types/id_wallets.rb +8 -0
- data/lib/didit/types/investigator.rb +221 -0
- data/lib/didit/types/ip.rb +8 -0
- data/lib/didit/types/kyb.rb +1391 -0
- data/lib/didit/types/lists.rb +111 -0
- data/lib/didit/types/monitoring_events.rb +235 -0
- data/lib/didit/types/networks.rb +120 -0
- data/lib/didit/types/passive_liveness.rb +95 -0
- data/lib/didit/types/phone.rb +69 -0
- data/lib/didit/types/proof_of_address.rb +166 -0
- data/lib/didit/types/questionnaires.rb +29 -0
- data/lib/didit/types/regulatory_reports.rb +100 -0
- data/lib/didit/types/report_templates.rb +41 -0
- data/lib/didit/types/sessions.rb +1441 -0
- data/lib/didit/types/transactions.rb +245 -0
- data/lib/didit/types/travel_rule.rb +168 -0
- data/lib/didit/types/users.rb +180 -0
- data/lib/didit/types/wallet_screening.rb +49 -0
- data/lib/didit/types/webhooks.rb +355 -0
- data/lib/didit/types/workflows.rb +32 -0
- data/lib/didit/types.rb +1681 -0
- data/lib/didit/version.rb +5 -0
- data/lib/didit/webhooks/canonical_json.rb +187 -0
- data/lib/didit/webhooks/client_methods.rb +23 -0
- data/lib/didit/webhooks/events.rb +221 -0
- data/lib/didit/webhooks/headers.rb +83 -0
- data/lib/didit/webhooks/verifier.rb +260 -0
- data/lib/didit/webhooks.rb +99 -0
- data/lib/didit-sdk.rb +4 -0
- data/lib/didit.rb +31 -0
- data/sig/didit/authentication/base64_url.rbs +10 -0
- data/sig/didit/authentication/cache.rbs +60 -0
- data/sig/didit/authentication/http.rbs +37 -0
- data/sig/didit/authentication/issuer.rbs +17 -0
- data/sig/didit/authentication/jws.rbs +41 -0
- data/sig/didit/authentication/jwt.rbs +14 -0
- data/sig/didit/authentication/key_set.rbs +25 -0
- data/sig/didit/authentication/pkce.rbs +20 -0
- data/sig/didit/authentication/types.rbs +114 -0
- data/sig/didit/authentication.rbs +81 -0
- data/sig/didit/client.rbs +61 -0
- data/sig/didit/errors.rbs +80 -0
- data/sig/didit/file_part.rbs +19 -0
- data/sig/didit/internal.rbs +199 -0
- data/sig/didit/model.rbs +42 -0
- data/sig/didit/page.rbs +34 -0
- data/sig/didit/raw_response.rbs +20 -0
- data/sig/didit/resource.rbs +8 -0
- data/sig/didit/resources/account.rbs +23 -0
- data/sig/didit/resources/address_validation.rbs +9 -0
- data/sig/didit/resources/age_estimation.rbs +9 -0
- data/sig/didit/resources/aml.rbs +10 -0
- data/sig/didit/resources/aml_monitoring.rbs +9 -0
- data/sig/didit/resources/analytics.rbs +9 -0
- data/sig/didit/resources/bank.rbs +9 -0
- data/sig/didit/resources/billing.rbs +10 -0
- data/sig/didit/resources/biometric_templates.rbs +13 -0
- data/sig/didit/resources/blocklist.rbs +11 -0
- data/sig/didit/resources/business_sessions.rbs +9 -0
- data/sig/didit/resources/businesses.rbs +22 -0
- data/sig/didit/resources/cases.rbs +103 -0
- data/sig/didit/resources/customization.rbs +10 -0
- data/sig/didit/resources/database_validation.rbs +9 -0
- data/sig/didit/resources/document_ai.rbs +9 -0
- data/sig/didit/resources/email.rbs +11 -0
- data/sig/didit/resources/face_match.rbs +9 -0
- data/sig/didit/resources/face_search.rbs +9 -0
- data/sig/didit/resources/fraud_checks.rbs +11 -0
- data/sig/didit/resources/id_verification.rbs +9 -0
- data/sig/didit/resources/id_wallets.rbs +37 -0
- data/sig/didit/resources/investigator.rbs +23 -0
- data/sig/didit/resources/ip.rbs +9 -0
- data/sig/didit/resources/kyb.rbs +12 -0
- data/sig/didit/resources/lists.rbs +20 -0
- data/sig/didit/resources/monitoring_events.rbs +16 -0
- data/sig/didit/resources/networks.rbs +25 -0
- data/sig/didit/resources/passive_liveness.rbs +9 -0
- data/sig/didit/resources/phone.rbs +11 -0
- data/sig/didit/resources/pricing.rbs +9 -0
- data/sig/didit/resources/proof_of_address.rbs +9 -0
- data/sig/didit/resources/questionnaires.rbs +13 -0
- data/sig/didit/resources/regulatory_reports.rbs +16 -0
- data/sig/didit/resources/report_templates.rbs +13 -0
- data/sig/didit/resources/sessions.rbs +78 -0
- data/sig/didit/resources/transactions.rbs +43 -0
- data/sig/didit/resources/travel_rule.rbs +62 -0
- data/sig/didit/resources/users.rbs +21 -0
- data/sig/didit/resources/wallet_screening.rbs +11 -0
- data/sig/didit/resources/webhooks.rbs +23 -0
- data/sig/didit/resources/workflows.rbs +13 -0
- data/sig/didit/resources.rbs +50 -0
- data/sig/didit/types/account.rbs +22 -0
- data/sig/didit/types/account_components.rbs +94 -0
- data/sig/didit/types/address_validation.rbs +7 -0
- data/sig/didit/types/age_estimation.rbs +80 -0
- data/sig/didit/types/aml.rbs +174 -0
- data/sig/didit/types/aml_monitoring.rbs +67 -0
- data/sig/didit/types/analytics.rbs +57 -0
- data/sig/didit/types/bank.rbs +9 -0
- data/sig/didit/types/billing.rbs +19 -0
- data/sig/didit/types/biometric_templates.rbs +268 -0
- data/sig/didit/types/blocklist.rbs +33 -0
- data/sig/didit/types/businesses.rbs +54 -0
- data/sig/didit/types/cases.rbs +414 -0
- data/sig/didit/types/components.rbs +10920 -0
- data/sig/didit/types/customization.rbs +7 -0
- data/sig/didit/types/database_validation.rbs +152 -0
- data/sig/didit/types/document_ai.rbs +123 -0
- data/sig/didit/types/email.rbs +112 -0
- data/sig/didit/types/face_match.rbs +86 -0
- data/sig/didit/types/face_search.rbs +112 -0
- data/sig/didit/types/fraud_checks.rbs +9 -0
- data/sig/didit/types/id_verification.rbs +196 -0
- data/sig/didit/types/id_wallets.rbs +55 -0
- data/sig/didit/types/investigator.rbs +212 -0
- data/sig/didit/types/ip.rbs +7 -0
- data/sig/didit/types/kyb.rbs +1254 -0
- data/sig/didit/types/lists.rbs +106 -0
- data/sig/didit/types/monitoring_events.rbs +234 -0
- data/sig/didit/types/networks.rbs +113 -0
- data/sig/didit/types/passive_liveness.rbs +87 -0
- data/sig/didit/types/phone.rbs +65 -0
- data/sig/didit/types/proof_of_address.rbs +144 -0
- data/sig/didit/types/questionnaires.rbs +30 -0
- data/sig/didit/types/regulatory_reports.rbs +101 -0
- data/sig/didit/types/report_templates.rbs +42 -0
- data/sig/didit/types/sessions.rbs +1349 -0
- data/sig/didit/types/transactions.rbs +273 -0
- data/sig/didit/types/travel_rule.rbs +188 -0
- data/sig/didit/types/users.rbs +181 -0
- data/sig/didit/types/wallet_screening.rbs +40 -0
- data/sig/didit/types/webhooks.rbs +311 -0
- data/sig/didit/types/workflows.rbs +30 -0
- data/sig/didit/webhooks/canonical_json.rbs +25 -0
- data/sig/didit/webhooks/client_methods.rbs +11 -0
- data/sig/didit/webhooks/events.rbs +125 -0
- data/sig/didit/webhooks/headers.rbs +22 -0
- data/sig/didit/webhooks/verifier.rbs +54 -0
- data/sig/didit/webhooks.rbs +37 -0
- data/sig/didit.rbs +20 -0
- metadata +300 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 4537c78240a3127727b1208e521b36f946950a9b6a168687ec9c95966834f5ee
|
|
4
|
+
data.tar.gz: 8946035f920824815b730d9903962b98e991a9540b8e4acacdb628a8b3a475a5
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 62db2a22d9e118145679b32b4d6f82cfee433e46636296d43da9f374968222fcefa44a7b8d04a1ead2c43a8c97bc939f5686884b8a62e09c2a6e99fcda274e44
|
|
7
|
+
data.tar.gz: 2d7e3ad74748b9996661bdde816fee61e2fbb7265141518dedef948b9638c4725e2eb3fdbda5d658cec7293c5eaee69d506232e4b48a2eedeb2cc9cf946157cc
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [1.0.0](https://github.com/didit-protocol/sdk-ruby/compare/v0.1.0...v1.0.0) (2026-10-29)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### ⚠ BREAKING CHANGES
|
|
7
|
+
|
|
8
|
+
* **spec:** the API spec sync changes the SDK surface (nullable fields, removed fields, account.organizations.list returns a page, the session decision response is the SessionDecision component).
|
|
9
|
+
|
|
10
|
+
### Features
|
|
11
|
+
|
|
12
|
+
* add bank.account_check (POST /v3/bank/account-check/) and bank_verifications on the decision ([de3a7b8](https://github.com/didit-protocol/sdk-ruby/commit/de3a7b85d29409df6e8a4eeef793c4fbe00a0904))
|
|
13
|
+
* **spec:** sync API spec 2026-10-23 ([#4](https://github.com/didit-protocol/sdk-ruby/issues/4)) ([ce9061c](https://github.com/didit-protocol/sdk-ruby/commit/ce9061c3056274319f5ab9325dba5c8439cc048a))
|
|
14
|
+
* **spec:** sync API spec 2026-10-27 ([#13](https://github.com/didit-protocol/sdk-ruby/issues/13)) ([a7ab2a9](https://github.com/didit-protocol/sdk-ruby/commit/a7ab2a9624d64dfda1c3fa2fb03cd93605523fee))
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
### API Spec Updates
|
|
18
|
+
|
|
19
|
+
* **spec:** sync API spec 2026-10-25 ([#8](https://github.com/didit-protocol/sdk-ruby/issues/8)) ([2e941a5](https://github.com/didit-protocol/sdk-ruby/commit/2e941a58a29b950f066e9d879f77342a715e9cae))
|
|
20
|
+
* **spec:** sync API spec 2026-10-29 ([#15](https://github.com/didit-protocol/sdk-ruby/issues/15)) ([468dbfc](https://github.com/didit-protocol/sdk-ruby/commit/468dbfc3137c4a84021ce1f51a04c2a689b668bd))
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
### Miscellaneous Chores
|
|
24
|
+
|
|
25
|
+
* release 1.0.0 ([b183cb4](https://github.com/didit-protocol/sdk-ruby/commit/b183cb463fc00c66d15ab6403f2ecb79c58b49a5))
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Didit
|
|
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,277 @@
|
|
|
1
|
+
# Didit Ruby SDK
|
|
2
|
+
|
|
3
|
+
The official [Didit](https://didit.me) Ruby library.
|
|
4
|
+
Identity verification (KYC), business verification (KYB), AML screening, biometrics, transaction monitoring, Travel Rule and customer authentication, from your backend.
|
|
5
|
+
|
|
6
|
+
> In development. Not released yet.
|
|
7
|
+
|
|
8
|
+
- Every Didit server API: sessions, standalone checks, KYB, users and businesses, workflows, webhooks, cases, transactions, wallet screening, Travel Rule and account management.
|
|
9
|
+
- Typed results with RBS signatures (checked with Steep), generated from the [OpenAPI contract](openapi/).
|
|
10
|
+
- Webhook verification and Didit Authentication (OpenID Connect) token verification built in.
|
|
11
|
+
- Standard library `net/http` with persistent connections, retries and timeouts. The only dependency is `jwt`, and only the Authentication module loads it.
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
```ruby
|
|
16
|
+
# Gemfile
|
|
17
|
+
gem "didit-sdk"
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
```sh
|
|
21
|
+
gem install didit-sdk
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
```ruby
|
|
25
|
+
require "didit"
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Ruby 3.1 or newer (tested on 3.1, 3.2, 3.3, 3.4 and 4.0).
|
|
29
|
+
|
|
30
|
+
## Quickstart
|
|
31
|
+
|
|
32
|
+
Create a session, send the user to its URL, read the decision and verify the webhook Didit sends when it changes.
|
|
33
|
+
|
|
34
|
+
```ruby
|
|
35
|
+
require "didit"
|
|
36
|
+
|
|
37
|
+
didit = Didit::Client.new # reads DIDIT_API_KEY
|
|
38
|
+
|
|
39
|
+
session = didit.sessions.create(workflow_id: "your-workflow-id", vendor_data: "user-42")
|
|
40
|
+
redirect_to session.url
|
|
41
|
+
|
|
42
|
+
decision = didit.sessions.retrieve_decision(session.session_id)
|
|
43
|
+
decision.status # => "Approved", "Declined", "In Review", ...
|
|
44
|
+
decision.id_verifications&.first&.first_name
|
|
45
|
+
|
|
46
|
+
# In your webhook endpoint (Rails), with the raw body:
|
|
47
|
+
event = didit.webhooks.verify(request.raw_post, request.headers, ENV.fetch("DIDIT_WEBHOOK_SECRET"))
|
|
48
|
+
case event
|
|
49
|
+
in Didit::Webhooks::StatusUpdatedEvent(status: "Approved", session_id:) then approve(session_id)
|
|
50
|
+
in Didit::Webhooks::StatusUpdatedEvent then nil
|
|
51
|
+
in Didit::Webhooks::UnknownEvent then nil # a type this SDK version does not know yet
|
|
52
|
+
else nil
|
|
53
|
+
end
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
[`examples/quickstart.rb`](examples/quickstart.rb) runs the same steps. [`examples/`](examples/) also has standalone ID verification, AML screening, transaction screening, Travel Rule, Authentication, and webhook receivers for Rails, Sinatra and Rack.
|
|
57
|
+
|
|
58
|
+
## Configuration
|
|
59
|
+
|
|
60
|
+
```ruby
|
|
61
|
+
didit = Didit::Client.new(
|
|
62
|
+
api_key: "...", # default ENV["DIDIT_API_KEY"]
|
|
63
|
+
organization_id: "...", # default ENV["DIDIT_ORGANIZATION_ID"]; needed by application-scoped methods
|
|
64
|
+
application_id: "...", # default ENV["DIDIT_APPLICATION_ID"]; needed by application-scoped methods
|
|
65
|
+
base_url: "https://verification.didit.me",
|
|
66
|
+
account_base_url: "https://apx.didit.me/auth/v2",
|
|
67
|
+
timeout: 60, # seconds per attempt
|
|
68
|
+
max_retries: 2,
|
|
69
|
+
default_headers: {},
|
|
70
|
+
logger: nil, # any object with #debug; or DIDIT_LOG=debug for stderr
|
|
71
|
+
http_client: nil # ->(uri) { Net::HTTP.new(...) } for proxies or a CA bundle
|
|
72
|
+
)
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
- One client per process is the intended use. It is thread safe, keeps connections open per host, and a forked child never reuses its parent's connections.
|
|
76
|
+
- `with_options` returns a copy with some options changed that shares the connections: `didit.with_options(timeout: 5, max_retries: 0)`.
|
|
77
|
+
- Every method takes `request_options:` for one call: `{ timeout:, max_retries:, extra_headers:, extra_query:, extra_body: }`.
|
|
78
|
+
- Application-scoped methods (all of `cases`, `networks`, `regulatory_reports`, `report_templates` and `business_sessions`, and some of `sessions`, `users`, `businesses` and `transactions`): the SDK fills `organization_id` and `application_id` from the client, and raises `Didit::ConfigurationError` before sending when either is missing.
|
|
79
|
+
- The API key is sent only to `base_url`. It never appears in logs, error messages or error bodies.
|
|
80
|
+
|
|
81
|
+
## Making calls
|
|
82
|
+
|
|
83
|
+
Resources follow the API: `didit.sessions`, `didit.sessions.imports`, `didit.users`, `didit.transactions.rules`, `didit.cases.notes`, and so on.
|
|
84
|
+
Ids in the URL are positional arguments; everything else is a keyword.
|
|
85
|
+
|
|
86
|
+
```ruby
|
|
87
|
+
didit.users.update("user-42", display_name: "Maria", metadata: { plan: "gold" })
|
|
88
|
+
didit.sessions.update_status(session_id, new_status: "Declined", comment: nil) # nil sends JSON null
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
A keyword you leave out is not sent. Unknown or missing keywords raise `ArgumentError`, like any Ruby method.
|
|
92
|
+
Nested objects are Hashes with the API's field names, as symbols or strings.
|
|
93
|
+
|
|
94
|
+
Results are typed objects:
|
|
95
|
+
|
|
96
|
+
```ruby
|
|
97
|
+
session.session_id # typed reader
|
|
98
|
+
session[:session_id] # any field, also ones this SDK version does not know yet
|
|
99
|
+
session.to_h # the JSON object with symbol keys
|
|
100
|
+
session.created_at # Time
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
`didit.with_raw_response` gives the HTTP response behind a result:
|
|
104
|
+
|
|
105
|
+
```ruby
|
|
106
|
+
raw = didit.with_raw_response.sessions.create(workflow_id: "...")
|
|
107
|
+
raw.status # => 201
|
|
108
|
+
raw.request_id # => "..."
|
|
109
|
+
session = raw.parse
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Errors
|
|
113
|
+
|
|
114
|
+
| Error | When |
|
|
115
|
+
|---|---|
|
|
116
|
+
| `Didit::DiditError` | base of everything below |
|
|
117
|
+
| `Didit::APIConnectionError` / `Didit::APITimeoutError` | no response |
|
|
118
|
+
| `Didit::APIStatusError` | any non-2xx, with `status`, `body`, `headers`, `request_id` |
|
|
119
|
+
| `BadRequestError` 400, `AuthenticationError` 401, `PermissionDeniedError` 403, `NotFoundError` 404, `ConflictError` 409, `UnprocessableEntityError` 422, `RateLimitError` 429, `InternalServerError` 5xx | subclasses of `APIStatusError` |
|
|
120
|
+
| `Didit::APIResponseValidationError` | a 2xx that is not the documented content, or a pagination link that loops or leaves the API |
|
|
121
|
+
| `Didit::WebhookVerificationError` | webhook verification failed, with a `reason` |
|
|
122
|
+
| `Didit::TokenVerificationError` | an Authentication token failed verification, with a `reason` |
|
|
123
|
+
| `Didit::ConfigurationError` | missing API key or ids, or an invalid argument, before anything is sent |
|
|
124
|
+
|
|
125
|
+
```ruby
|
|
126
|
+
begin
|
|
127
|
+
didit.sessions.retrieve_decision(session_id)
|
|
128
|
+
rescue Didit::NotFoundError
|
|
129
|
+
# ...
|
|
130
|
+
rescue Didit::APIStatusError => e
|
|
131
|
+
logger.warn("Didit #{e.status}: #{e.message}") # the message ends with the request id
|
|
132
|
+
end
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## Retries and timeouts
|
|
136
|
+
|
|
137
|
+
Failed requests are retried up to `max_retries` times (default 2) with exponential backoff from 0.5 s, full jitter, capped at 8 s, honouring `Retry-After` (capped at 60 s).
|
|
138
|
+
GET, HEAD, PUT and DELETE are retried on connection errors, timeouts, 408, 429 and 5xx.
|
|
139
|
+
POST and PATCH are retried only on 429 and when the connection failed before the request was sent, never after it may have reached the server.
|
|
140
|
+
|
|
141
|
+
## Pagination
|
|
142
|
+
|
|
143
|
+
```ruby
|
|
144
|
+
page = didit.sessions.list(limit: 50)
|
|
145
|
+
page.total_count # the API's count across pages
|
|
146
|
+
page.each { |s| puts s.status } # this page
|
|
147
|
+
page.next_page # the next Didit::Page, or nil
|
|
148
|
+
|
|
149
|
+
didit.sessions.list(limit: 50).auto_paging_each { |s| puts s.session_id } # every page, fetched as needed
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## Files and binary responses
|
|
153
|
+
|
|
154
|
+
Upload arguments take a `Pathname`, an `IO` (`File`, `StringIO`, ...), a `String` with the file's bytes, or a `Didit::FilePart` to set the file name and content type:
|
|
155
|
+
|
|
156
|
+
```ruby
|
|
157
|
+
didit.id_verification.verify(front_image: Pathname("passport.jpg"))
|
|
158
|
+
didit.face_match.compare(user_image: File.open("selfie.jpg", "rb"), ref_image: Didit::FilePart.new(bytes, filename: "ref.png"))
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Reports come back as bytes, or in chunks to a block:
|
|
162
|
+
|
|
163
|
+
```ruby
|
|
164
|
+
pdf = didit.sessions.generate_pdf(session_id)
|
|
165
|
+
File.open("report.pdf", "wb") { |f| didit.sessions.generate_pdf(session_id) { |chunk| f.write(chunk) } }
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
## Webhooks
|
|
169
|
+
|
|
170
|
+
```ruby
|
|
171
|
+
event = Didit::Webhooks.verify(raw_body, headers, secret) # or didit.webhooks.verify(...)
|
|
172
|
+
event = Didit::Webhooks.verify(raw_body, headers, [new_secret, old_secret]) # during a rotation
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
- Pass the raw request body: `request.raw_post` in Rails, `request.body.read` after `request.body.rewind` in Sinatra (it may have read the body for its params), `env["rack.input"].read` in Rack. Headers can be a Hash in any case, a Rack env or Rails `request.headers`.
|
|
176
|
+
- A delivery is accepted when `X-Signature` matches the body, `X-Signature-V2` matches its canonical JSON, or a `-Previous` variant matches (secret rotation). Timestamps older or newer than `tolerance:` (300 s) are rejected.
|
|
177
|
+
- `verify_parsed(payload, headers, secret)` is for frameworks that only give you the parsed JSON; it can check `X-Signature-V2` only.
|
|
178
|
+
- The deprecated `X-Signature-Simple` counts only with `allow_simple: true`; then `event.body_authenticated?` is false and you should re-fetch the record from the API.
|
|
179
|
+
- Failures raise `Didit::WebhookVerificationError` with `reason`: `:missing_signature`, `:invalid_signature`, `:missing_timestamp`, `:timestamp_mismatch`, `:timestamp_out_of_tolerance` or `:invalid_payload`.
|
|
180
|
+
- Events are typed: `StatusUpdatedEvent`, `DataUpdatedEvent`, `UserStatusUpdatedEvent`, `UserDataUpdatedEvent`, `BusinessStatusUpdatedEvent`, `BusinessDataUpdatedEvent`, `ActivityCreatedEvent`, `TransactionCreatedEvent`, `TransactionStatusUpdatedEvent`, `TravelRuleStatusUpdatedEvent`, `WorkflowComplianceChangedEvent`, `KybRegistrySearchResolvedEvent`, and `UnknownEvent` for anything else. Session events carry the typed `decision`.
|
|
181
|
+
- `kyb.registry_search.resolved` callbacks arrive unsigned, so `verify` rejects them (with `:missing_signature`, or a timestamp reason when their timestamp fails the checks that come first). `Didit::Webhooks.parse_event(JSON.parse(body))` reads one without verifying it; treat it as untrusted and re-fetch the data from the API.
|
|
182
|
+
|
|
183
|
+
See [`examples/webhooks/`](examples/webhooks/) for Rails, Sinatra and Rack receivers. In a Rails controller that inherits from `ActionController::Base`, skip CSRF protection for the webhook action.
|
|
184
|
+
|
|
185
|
+
## Transactions and Travel Rule
|
|
186
|
+
|
|
187
|
+
```ruby
|
|
188
|
+
txn = didit.transactions.create(
|
|
189
|
+
transaction_id: "order-1001", transaction_category: "finance",
|
|
190
|
+
transaction_details: { direction: "outbound", amount: 1500, currency: "EUR", action_type: "withdrawal" },
|
|
191
|
+
subject: { entity_type: "individual", vendor_data: "user-42", full_name: "Maria Garcia" }
|
|
192
|
+
)
|
|
193
|
+
txn.status
|
|
194
|
+
didit.transactions.rules.list
|
|
195
|
+
didit.transactions.travel_rule.update_transfer(txn.txn_id, payment_txn_id: "0xabc...")
|
|
196
|
+
didit.wallet_screening.screen(wallet_address: "0x...", blockchain: "ethereum")
|
|
197
|
+
didit.travel_rule.list_vasps(search: "exchange")
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Webhooks `transaction.created`, `transaction.status.updated` and `travel_rule.status.updated` arrive as typed events.
|
|
201
|
+
|
|
202
|
+
## Authentication
|
|
203
|
+
|
|
204
|
+
Didit Authentication gives your application an OpenID Connect issuer for your users. The `Didit::Authentication` module is for your backend: it verifies the tokens your frontend sends, exchanges codes, refreshes tokens, checks sessions and creates step-up verifications. It never uses the API key.
|
|
205
|
+
|
|
206
|
+
```ruby
|
|
207
|
+
auth = Didit::Authentication.new("https://acme.auth.didit.me", client_id: ENV["DIDIT_APPLICATION_ID"])
|
|
208
|
+
# or Didit::Authentication.from_publishable_key("pk_live_...")
|
|
209
|
+
|
|
210
|
+
claims = auth.verify_access_token(bearer_token) # ES256, typ at+jwt, iss, exp/nbf/iat with 60 s leeway
|
|
211
|
+
claims.sub
|
|
212
|
+
claims.org&.role
|
|
213
|
+
|
|
214
|
+
pkce = Didit::Authentication.create_pkce # redirect with pkce.code_challenge, state and nonce
|
|
215
|
+
tokens = auth.exchange_code(params[:code], code_verifier: pkce.code_verifier, redirect_uri: callback_url)
|
|
216
|
+
id = auth.verify_id_token(tokens.id_token, nonce: session[:nonce])
|
|
217
|
+
tokens = auth.refresh(tokens.refresh_token) # serialise refreshes per session: reusing one revokes it
|
|
218
|
+
|
|
219
|
+
auth.get_session(access_token) # instant revocation check
|
|
220
|
+
auth.revoke_session(access_token)
|
|
221
|
+
step_up = auth.create_step_up(access_token, action: "payout", amount: "250.00", currency: "EUR", return_to: url)
|
|
222
|
+
auth.retrieve_step_up(access_token, step_up.id).approved? # act only on status "approved"
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
Discovery and keys are fetched on first use and cached as the issuer's `Cache-Control` says; an unknown key id triggers at most one refetch per minute.
|
|
226
|
+
Rejected tokens raise `Didit::TokenVerificationError` with a `reason` such as `:unsupported_algorithm`, `:invalid_issuer`, `:invalid_audience`, `:expired` or `:unknown_key`.
|
|
227
|
+
|
|
228
|
+
## Account management
|
|
229
|
+
|
|
230
|
+
The `didit.account` methods use the account API (`https://apx.didit.me/auth/v2`) and a Bearer token instead of the API key:
|
|
231
|
+
|
|
232
|
+
```ruby
|
|
233
|
+
login = didit.account.login(email: "you@example.com", password: "...")
|
|
234
|
+
account = didit.with_options(account_token: login.access_token)
|
|
235
|
+
account.account.organizations.list
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
## Debug logging
|
|
239
|
+
|
|
240
|
+
`DIDIT_LOG=debug` prints one line per request to stderr: method, path, status, duration and request id. It never prints bodies, headers or query strings. Pass `logger:` to send the same lines to your logger.
|
|
241
|
+
|
|
242
|
+
## Language notes
|
|
243
|
+
|
|
244
|
+
Choices this SDK makes where [docs/DESIGN.md](docs/DESIGN.md) leaves the language idiom open:
|
|
245
|
+
|
|
246
|
+
- **Arguments.** Ids in the path are positional; request fields are keywords. An omitted keyword is left out of the request and `nil` sends JSON `null`. `Didit::OMIT` leaves a keyword out explicitly, which helps when building calls conditionally. Keywords that are Ruby keywords (`retry:`, `case:`) work as usual.
|
|
247
|
+
- **Names.** Methods and readers are snake_case. A field that would hide an `Object` method gets a trailing underscore (`method` is `method_`); camelCase fields read as snake_case (`firstName` is `first_name`). `model[:wireName]` always gives the raw value.
|
|
248
|
+
- **Types.** Responses are `Didit::Model` subclasses under `Didit::Types`, loaded on first use. Readers convert on access and never raise: a value with an unexpected shape comes back as it is. Date-times are `Time`, dates are `Date`, numbers are `Float`, and string decimals stay strings. Models support `to_h`, `to_json`, `==` and pattern matching (`in {status: "Approved"}`).
|
|
249
|
+
- **Open enums.** Known values are frozen string constants (`Didit::Types::SessionDecisionStatus::APPROVED`, plus `VALUES`); values the SDK does not know are returned unchanged.
|
|
250
|
+
- **Signatures.** RBS signatures ship in `sig/` with the gem, so Steep users get typed calls and results. Readers of required fields are typed non-nil; if the API ever omits one, the reader returns nil.
|
|
251
|
+
- **Pages.** `Didit::Page` is `Enumerable` over the current page. Use `total_count` for the API's `count` (because `Enumerable#count` counts this page), `next_url`, `previous_url`, `next_page` and `auto_paging_each`. A `next` link to another origin, or one already followed, raises `APIResponseValidationError`. Lists that page with `next_offset` (case action receipts and duplicates) return plain results.
|
|
252
|
+
- **Errors.** Error names follow DESIGN. `APIResponseValidationError` (Python's name) covers 2xx responses that are not JSON and bad pagination links. Every check made before sending raises `ConfigurationError`, including invalid path ids (empty, `.` or `..`), uploads and request options. Verification `reason`s are Symbols.
|
|
253
|
+
- **Timeouts.** `timeout` applies to each attempt's connect, write and read separately, as net/http does; there is no overall deadline.
|
|
254
|
+
- **HTTP.** net/http with a small connection pool per origin. Redirects are never followed. Proxies come from the usual `http_proxy`/`https_proxy` variables, or from `http_client:`, a callable that returns a configured, not yet started `Net::HTTP` for a URI.
|
|
255
|
+
- **Uploads.** Files are read into memory before sending, so a retry can resend them. Content types come from a fixed table of file extensions, not the host's MIME database.
|
|
256
|
+
- **Deprecations.** Methods for operations removed from the API stay for one minor version and warn once through Ruby's `:deprecated` warning category (shown with `Warning[:deprecated] = true` or `ruby -W:deprecated`).
|
|
257
|
+
- **Webhooks.** The canonical JSON is serialised by the SDK itself, because Ruby prints floats differently from Python (`1.0e-05` instead of `1e-05`). `event.verified_by` names the header that matched. `verify` and `verify_parsed` take a `now:` keyword for tests.
|
|
258
|
+
- **Authentication.** The `jwt` gem checks the ES256 signature; the SDK checks everything else. `verify_id_token(token, nonce: nil)` requires a token without a nonce (the refresh case). A step-up's `method` field reads as `verification_method`. `clock:` (unix seconds) is for tests. The issuer is configured without a trailing slash; if its discovery document names it with one, tokens must carry that exact `iss`. A discovery document that names another issuer raises `ConfigurationError` and is fetched again only after a minute. Publishable keys are read with or without `=` padding.
|
|
259
|
+
|
|
260
|
+
## Development
|
|
261
|
+
|
|
262
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md): generated versus hand-written code, `bundle exec rake generate`, tests, the daily spec sync and releases.
|
|
263
|
+
|
|
264
|
+
## Versioning
|
|
265
|
+
|
|
266
|
+
[Semantic versioning](https://semver.org). The SDK stays on 0.x until its first public release; while on 0.x, a breaking change bumps the minor version. Releases are cut by release-please and the changelog is generated.
|
|
267
|
+
|
|
268
|
+
## Links
|
|
269
|
+
|
|
270
|
+
- API reference and guides: https://docs.didit.me
|
|
271
|
+
- Webhooks: https://docs.didit.me/integration/webhooks
|
|
272
|
+
- Design contract shared by every Didit server SDK: [docs/DESIGN.md](docs/DESIGN.md)
|
|
273
|
+
- Security: [SECURITY.md](SECURITY.md)
|
|
274
|
+
|
|
275
|
+
## License
|
|
276
|
+
|
|
277
|
+
MIT
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Didit
|
|
4
|
+
class Authentication
|
|
5
|
+
# Unpadded base64url (RFC 4648 section 5) with strict decoding.
|
|
6
|
+
# @api private
|
|
7
|
+
module Base64Url
|
|
8
|
+
ALPHABET = /\A[A-Za-z0-9_-]*\z/
|
|
9
|
+
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
# @param bytes [String]
|
|
13
|
+
# @return [String]
|
|
14
|
+
def encode(bytes)
|
|
15
|
+
[bytes].pack("m0").tr("+/", "-_").delete("=")
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# The bytes of canonical unpadded base64url, else nil: no padding, no
|
|
19
|
+
# characters outside the alphabet, no impossible length, no stray bits.
|
|
20
|
+
#
|
|
21
|
+
# @param text [String]
|
|
22
|
+
# @return [String, nil] binary
|
|
23
|
+
def decode(text)
|
|
24
|
+
text = text.b
|
|
25
|
+
return nil unless ALPHABET.match?(text) && text.length % 4 != 1
|
|
26
|
+
|
|
27
|
+
bytes = (text.tr("-_", "+/") + ("=" * (-text.length % 4))).unpack1("m0")
|
|
28
|
+
bytes if bytes.is_a?(String)
|
|
29
|
+
rescue ArgumentError
|
|
30
|
+
nil
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Didit
|
|
4
|
+
class Authentication
|
|
5
|
+
# The parts of +/.well-known/openid-configuration+ the SDK uses.
|
|
6
|
+
# @api private
|
|
7
|
+
class Discovery
|
|
8
|
+
# @return [String]
|
|
9
|
+
attr_reader :issuer
|
|
10
|
+
# @return [String]
|
|
11
|
+
attr_reader :token_endpoint
|
|
12
|
+
# @return [String]
|
|
13
|
+
attr_reader :jwks_uri
|
|
14
|
+
# @return [Hash] the document as the issuer sent it (frozen)
|
|
15
|
+
attr_reader :document
|
|
16
|
+
|
|
17
|
+
def initialize(issuer:, token_endpoint:, jwks_uri:, document:)
|
|
18
|
+
@issuer = issuer
|
|
19
|
+
@token_endpoint = token_endpoint
|
|
20
|
+
@jwks_uri = jwks_uri
|
|
21
|
+
@document = IssuerCache.deep_freeze(document)
|
|
22
|
+
freeze
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# What is cached for one issuer, and when to fetch again (no I/O here).
|
|
27
|
+
#
|
|
28
|
+
# The discovery document and the key set are cached for the response's
|
|
29
|
+
# +Cache-Control: max-age+ less its +Age+ (Didit sends +public, max-age=300+),
|
|
30
|
+
# five minutes when the header is absent, a minute for +no-store+ and
|
|
31
|
+
# +no-cache+, and never less than a minute or more than a day. A token whose
|
|
32
|
+
# +kid+ is not in the cached key set triggers a refetch only when the last
|
|
33
|
+
# fetch, successful or failed, is at least a minute old. After a failed
|
|
34
|
+
# fetch, verification that needs the keys fails fast for a minute instead of
|
|
35
|
+
# fetching again for every token. A discovery document that names another
|
|
36
|
+
# issuer is refused, without fetching it again, for a minute.
|
|
37
|
+
#
|
|
38
|
+
# The caller holds a lock around fetches; reads of a fresh entry need none,
|
|
39
|
+
# because entries are replaced, never changed.
|
|
40
|
+
# @api private
|
|
41
|
+
class IssuerCache
|
|
42
|
+
DEFAULT_MAX_AGE = 300
|
|
43
|
+
MIN_MAX_AGE = 60
|
|
44
|
+
MAX_MAX_AGE = 86_400
|
|
45
|
+
REFETCH_INTERVAL = 60
|
|
46
|
+
NO_CACHE = %w[no-store no-cache].freeze
|
|
47
|
+
|
|
48
|
+
# A cached value and the clock reading it expires at.
|
|
49
|
+
class Entry
|
|
50
|
+
attr_reader :value, :expires_at
|
|
51
|
+
|
|
52
|
+
def initialize(value, expires_at)
|
|
53
|
+
@value = value
|
|
54
|
+
@expires_at = expires_at
|
|
55
|
+
freeze
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
class << self
|
|
60
|
+
# How long a discovery or JWKS response may be kept, in seconds.
|
|
61
|
+
#
|
|
62
|
+
# @param headers [Hash{String => String}] names in lower case
|
|
63
|
+
# @return [Integer]
|
|
64
|
+
def cache_seconds(headers)
|
|
65
|
+
max_age = max_age(headers.fetch("cache-control", ""))
|
|
66
|
+
return DEFAULT_MAX_AGE if max_age.nil?
|
|
67
|
+
|
|
68
|
+
age = Integer(headers.fetch("age", "").strip, 10, exception: false) || 0
|
|
69
|
+
(max_age - age).clamp(MIN_MAX_AGE, MAX_MAX_AGE)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# A deeply frozen copy of a parsed JSON value.
|
|
73
|
+
def deep_freeze(value)
|
|
74
|
+
case value
|
|
75
|
+
when Hash then value.to_h { |key, item| [key.dup.freeze, deep_freeze(item)] }.freeze
|
|
76
|
+
when Array then value.map { |item| deep_freeze(item) }.freeze
|
|
77
|
+
when String then value.dup.freeze
|
|
78
|
+
else value
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
private
|
|
83
|
+
|
|
84
|
+
# The max-age directive, 0 for no-store and no-cache, nil without either.
|
|
85
|
+
def max_age(cache_control)
|
|
86
|
+
# @type var max_age: Integer?
|
|
87
|
+
max_age = nil
|
|
88
|
+
cache_control.split(",").each do |directive|
|
|
89
|
+
name, value = directive.split("=", 2)
|
|
90
|
+
name = name.to_s.strip.downcase
|
|
91
|
+
return 0 if NO_CACHE.include?(name)
|
|
92
|
+
|
|
93
|
+
if name == "max-age"
|
|
94
|
+
seconds = Integer(value.to_s.strip.delete_prefix('"').delete_suffix('"'), 10, exception: false)
|
|
95
|
+
max_age = seconds if seconds
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
max_age
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# @param clock [#call] returns the current time in seconds
|
|
103
|
+
def initialize(clock)
|
|
104
|
+
@clock = clock
|
|
105
|
+
@discovery = nil
|
|
106
|
+
@key_set = nil
|
|
107
|
+
@key_set_attempted_at = nil
|
|
108
|
+
@key_set_failed = false
|
|
109
|
+
@refused = nil
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# The cached discovery document while it is fresh.
|
|
113
|
+
#
|
|
114
|
+
# @return [Discovery, nil]
|
|
115
|
+
def discovery
|
|
116
|
+
fresh(@discovery)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# @return [Discovery]
|
|
120
|
+
def store_discovery(discovery, headers)
|
|
121
|
+
@discovery = Entry.new(discovery, now + IssuerCache.cache_seconds(headers))
|
|
122
|
+
discovery
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Notes that the discovery document named another issuer, with the error message.
|
|
126
|
+
def discovery_refused(message)
|
|
127
|
+
@refused = Entry.new(message, now + REFETCH_INTERVAL)
|
|
128
|
+
nil
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# The message of a discovery document refused less than a minute ago, else nil.
|
|
132
|
+
#
|
|
133
|
+
# @return [String, nil]
|
|
134
|
+
def refused_discovery
|
|
135
|
+
fresh(@refused)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# The cached key set while it is fresh.
|
|
139
|
+
#
|
|
140
|
+
# @return [KeySet, nil]
|
|
141
|
+
def key_set
|
|
142
|
+
fresh(@key_set)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# @return [KeySet]
|
|
146
|
+
def store_key_set(key_set, headers)
|
|
147
|
+
at = now
|
|
148
|
+
@key_set = Entry.new(key_set, at + IssuerCache.cache_seconds(headers))
|
|
149
|
+
@key_set_attempted_at = at
|
|
150
|
+
@key_set_failed = false
|
|
151
|
+
key_set
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Notes a fetch of the key set (or of the discovery document before it) that failed.
|
|
155
|
+
def key_set_fetch_failed
|
|
156
|
+
@key_set_attempted_at = now
|
|
157
|
+
@key_set_failed = true
|
|
158
|
+
nil
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# No key set is fresh and the last fetch failed less than a minute ago.
|
|
162
|
+
def key_set_unavailable?
|
|
163
|
+
attempted_at = @key_set_attempted_at
|
|
164
|
+
return false unless @key_set_failed && attempted_at && key_set.nil?
|
|
165
|
+
|
|
166
|
+
now - attempted_at < REFETCH_INTERVAL
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# The fresh key set when it has +kid+: the lock-free fast path.
|
|
170
|
+
#
|
|
171
|
+
# @return [KeySet, nil]
|
|
172
|
+
def key_set_with(kid)
|
|
173
|
+
current = key_set
|
|
174
|
+
current if current&.key?(kid)
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# Whether verifying a token signed with +kid+ should fetch the key set: when
|
|
178
|
+
# none is fresh, or when +kid+ is unknown and the last fetch, successful or
|
|
179
|
+
# failed, was at least {REFETCH_INTERVAL} seconds ago. A nil +kid+ asks for
|
|
180
|
+
# any fresh key set. Check {#key_set_unavailable?} first.
|
|
181
|
+
def needs_key_set_fetch?(kid)
|
|
182
|
+
current = key_set
|
|
183
|
+
return true if current.nil?
|
|
184
|
+
return false if kid.nil? || current.key?(kid)
|
|
185
|
+
|
|
186
|
+
attempted_at = @key_set_attempted_at
|
|
187
|
+
attempted_at.nil? || now - attempted_at >= REFETCH_INTERVAL
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
private
|
|
191
|
+
|
|
192
|
+
def fresh(entry)
|
|
193
|
+
entry.value if entry && now < entry.expires_at
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def now
|
|
197
|
+
@clock.call.to_f
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
end
|