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.
Files changed (234) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +25 -0
  3. data/LICENSE +21 -0
  4. data/README.md +277 -0
  5. data/lib/didit/authentication/base64_url.rb +34 -0
  6. data/lib/didit/authentication/cache.rb +201 -0
  7. data/lib/didit/authentication/http.rb +150 -0
  8. data/lib/didit/authentication/issuer.rb +115 -0
  9. data/lib/didit/authentication/jws.rb +231 -0
  10. data/lib/didit/authentication/key_set.rb +89 -0
  11. data/lib/didit/authentication/pkce.rb +64 -0
  12. data/lib/didit/authentication/types.rb +175 -0
  13. data/lib/didit/authentication.rb +546 -0
  14. data/lib/didit/client.rb +211 -0
  15. data/lib/didit/errors.rb +153 -0
  16. data/lib/didit/file_part.rb +85 -0
  17. data/lib/didit/internal/config.rb +94 -0
  18. data/lib/didit/internal/errors.rb +83 -0
  19. data/lib/didit/internal/log.rb +41 -0
  20. data/lib/didit/internal/multipart.rb +60 -0
  21. data/lib/didit/internal/request.rb +322 -0
  22. data/lib/didit/internal/transport.rb +198 -0
  23. data/lib/didit/internal/util.rb +76 -0
  24. data/lib/didit/internal.rb +15 -0
  25. data/lib/didit/model.rb +199 -0
  26. data/lib/didit/page.rb +130 -0
  27. data/lib/didit/raw_response.rb +57 -0
  28. data/lib/didit/resource.rb +15 -0
  29. data/lib/didit/resources/account.rb +309 -0
  30. data/lib/didit/resources/address_validation.rb +73 -0
  31. data/lib/didit/resources/age_estimation.rb +110 -0
  32. data/lib/didit/resources/aml.rb +166 -0
  33. data/lib/didit/resources/aml_monitoring.rb +30 -0
  34. data/lib/didit/resources/analytics.rb +87 -0
  35. data/lib/didit/resources/bank.rb +65 -0
  36. data/lib/didit/resources/billing.rb +61 -0
  37. data/lib/didit/resources/biometric_templates.rb +234 -0
  38. data/lib/didit/resources/blocklist.rb +130 -0
  39. data/lib/didit/resources/business_sessions.rb +29 -0
  40. data/lib/didit/resources/businesses.rb +471 -0
  41. data/lib/didit/resources/cases.rb +2116 -0
  42. data/lib/didit/resources/customization.rb +153 -0
  43. data/lib/didit/resources/database_validation.rb +327 -0
  44. data/lib/didit/resources/document_ai.rb +145 -0
  45. data/lib/didit/resources/email.rb +254 -0
  46. data/lib/didit/resources/face_match.rb +103 -0
  47. data/lib/didit/resources/face_search.rb +103 -0
  48. data/lib/didit/resources/fraud_checks.rb +130 -0
  49. data/lib/didit/resources/id_verification.rb +163 -0
  50. data/lib/didit/resources/id_wallets.rb +1468 -0
  51. data/lib/didit/resources/investigator.rb +419 -0
  52. data/lib/didit/resources/ip.rb +68 -0
  53. data/lib/didit/resources/kyb.rb +256 -0
  54. data/lib/didit/resources/lists.rb +332 -0
  55. data/lib/didit/resources/monitoring_events.rb +466 -0
  56. data/lib/didit/resources/networks.rb +427 -0
  57. data/lib/didit/resources/passive_liveness.rb +103 -0
  58. data/lib/didit/resources/phone.rb +271 -0
  59. data/lib/didit/resources/pricing.rb +40 -0
  60. data/lib/didit/resources/proof_of_address.rb +167 -0
  61. data/lib/didit/resources/questionnaires.rb +170 -0
  62. data/lib/didit/resources/regulatory_reports.rb +314 -0
  63. data/lib/didit/resources/report_templates.rb +146 -0
  64. data/lib/didit/resources/sessions.rb +2820 -0
  65. data/lib/didit/resources/transactions.rb +981 -0
  66. data/lib/didit/resources/travel_rule.rb +1202 -0
  67. data/lib/didit/resources/users.rb +521 -0
  68. data/lib/didit/resources/wallet_screening.rb +190 -0
  69. data/lib/didit/resources/webhooks.rb +396 -0
  70. data/lib/didit/resources/workflows.rb +214 -0
  71. data/lib/didit/resources.rb +263 -0
  72. data/lib/didit/types/account.rb +11 -0
  73. data/lib/didit/types/account_components.rb +105 -0
  74. data/lib/didit/types/address_validation.rb +8 -0
  75. data/lib/didit/types/age_estimation.rb +89 -0
  76. data/lib/didit/types/aml.rb +193 -0
  77. data/lib/didit/types/aml_monitoring.rb +73 -0
  78. data/lib/didit/types/analytics.rb +62 -0
  79. data/lib/didit/types/bank.rb +8 -0
  80. data/lib/didit/types/billing.rb +18 -0
  81. data/lib/didit/types/biometric_templates.rb +292 -0
  82. data/lib/didit/types/blocklist.rb +31 -0
  83. data/lib/didit/types/businesses.rb +45 -0
  84. data/lib/didit/types/cases.rb +368 -0
  85. data/lib/didit/types/components.rb +12206 -0
  86. data/lib/didit/types/customization.rb +8 -0
  87. data/lib/didit/types/database_validation.rb +177 -0
  88. data/lib/didit/types/document_ai.rb +135 -0
  89. data/lib/didit/types/email.rb +116 -0
  90. data/lib/didit/types/face_match.rb +93 -0
  91. data/lib/didit/types/face_search.rb +129 -0
  92. data/lib/didit/types/fraud_checks.rb +8 -0
  93. data/lib/didit/types/id_verification.rb +231 -0
  94. data/lib/didit/types/id_wallets.rb +8 -0
  95. data/lib/didit/types/investigator.rb +221 -0
  96. data/lib/didit/types/ip.rb +8 -0
  97. data/lib/didit/types/kyb.rb +1391 -0
  98. data/lib/didit/types/lists.rb +111 -0
  99. data/lib/didit/types/monitoring_events.rb +235 -0
  100. data/lib/didit/types/networks.rb +120 -0
  101. data/lib/didit/types/passive_liveness.rb +95 -0
  102. data/lib/didit/types/phone.rb +69 -0
  103. data/lib/didit/types/proof_of_address.rb +166 -0
  104. data/lib/didit/types/questionnaires.rb +29 -0
  105. data/lib/didit/types/regulatory_reports.rb +100 -0
  106. data/lib/didit/types/report_templates.rb +41 -0
  107. data/lib/didit/types/sessions.rb +1441 -0
  108. data/lib/didit/types/transactions.rb +245 -0
  109. data/lib/didit/types/travel_rule.rb +168 -0
  110. data/lib/didit/types/users.rb +180 -0
  111. data/lib/didit/types/wallet_screening.rb +49 -0
  112. data/lib/didit/types/webhooks.rb +355 -0
  113. data/lib/didit/types/workflows.rb +32 -0
  114. data/lib/didit/types.rb +1681 -0
  115. data/lib/didit/version.rb +5 -0
  116. data/lib/didit/webhooks/canonical_json.rb +187 -0
  117. data/lib/didit/webhooks/client_methods.rb +23 -0
  118. data/lib/didit/webhooks/events.rb +221 -0
  119. data/lib/didit/webhooks/headers.rb +83 -0
  120. data/lib/didit/webhooks/verifier.rb +260 -0
  121. data/lib/didit/webhooks.rb +99 -0
  122. data/lib/didit-sdk.rb +4 -0
  123. data/lib/didit.rb +31 -0
  124. data/sig/didit/authentication/base64_url.rbs +10 -0
  125. data/sig/didit/authentication/cache.rbs +60 -0
  126. data/sig/didit/authentication/http.rbs +37 -0
  127. data/sig/didit/authentication/issuer.rbs +17 -0
  128. data/sig/didit/authentication/jws.rbs +41 -0
  129. data/sig/didit/authentication/jwt.rbs +14 -0
  130. data/sig/didit/authentication/key_set.rbs +25 -0
  131. data/sig/didit/authentication/pkce.rbs +20 -0
  132. data/sig/didit/authentication/types.rbs +114 -0
  133. data/sig/didit/authentication.rbs +81 -0
  134. data/sig/didit/client.rbs +61 -0
  135. data/sig/didit/errors.rbs +80 -0
  136. data/sig/didit/file_part.rbs +19 -0
  137. data/sig/didit/internal.rbs +199 -0
  138. data/sig/didit/model.rbs +42 -0
  139. data/sig/didit/page.rbs +34 -0
  140. data/sig/didit/raw_response.rbs +20 -0
  141. data/sig/didit/resource.rbs +8 -0
  142. data/sig/didit/resources/account.rbs +23 -0
  143. data/sig/didit/resources/address_validation.rbs +9 -0
  144. data/sig/didit/resources/age_estimation.rbs +9 -0
  145. data/sig/didit/resources/aml.rbs +10 -0
  146. data/sig/didit/resources/aml_monitoring.rbs +9 -0
  147. data/sig/didit/resources/analytics.rbs +9 -0
  148. data/sig/didit/resources/bank.rbs +9 -0
  149. data/sig/didit/resources/billing.rbs +10 -0
  150. data/sig/didit/resources/biometric_templates.rbs +13 -0
  151. data/sig/didit/resources/blocklist.rbs +11 -0
  152. data/sig/didit/resources/business_sessions.rbs +9 -0
  153. data/sig/didit/resources/businesses.rbs +22 -0
  154. data/sig/didit/resources/cases.rbs +103 -0
  155. data/sig/didit/resources/customization.rbs +10 -0
  156. data/sig/didit/resources/database_validation.rbs +9 -0
  157. data/sig/didit/resources/document_ai.rbs +9 -0
  158. data/sig/didit/resources/email.rbs +11 -0
  159. data/sig/didit/resources/face_match.rbs +9 -0
  160. data/sig/didit/resources/face_search.rbs +9 -0
  161. data/sig/didit/resources/fraud_checks.rbs +11 -0
  162. data/sig/didit/resources/id_verification.rbs +9 -0
  163. data/sig/didit/resources/id_wallets.rbs +37 -0
  164. data/sig/didit/resources/investigator.rbs +23 -0
  165. data/sig/didit/resources/ip.rbs +9 -0
  166. data/sig/didit/resources/kyb.rbs +12 -0
  167. data/sig/didit/resources/lists.rbs +20 -0
  168. data/sig/didit/resources/monitoring_events.rbs +16 -0
  169. data/sig/didit/resources/networks.rbs +25 -0
  170. data/sig/didit/resources/passive_liveness.rbs +9 -0
  171. data/sig/didit/resources/phone.rbs +11 -0
  172. data/sig/didit/resources/pricing.rbs +9 -0
  173. data/sig/didit/resources/proof_of_address.rbs +9 -0
  174. data/sig/didit/resources/questionnaires.rbs +13 -0
  175. data/sig/didit/resources/regulatory_reports.rbs +16 -0
  176. data/sig/didit/resources/report_templates.rbs +13 -0
  177. data/sig/didit/resources/sessions.rbs +78 -0
  178. data/sig/didit/resources/transactions.rbs +43 -0
  179. data/sig/didit/resources/travel_rule.rbs +62 -0
  180. data/sig/didit/resources/users.rbs +21 -0
  181. data/sig/didit/resources/wallet_screening.rbs +11 -0
  182. data/sig/didit/resources/webhooks.rbs +23 -0
  183. data/sig/didit/resources/workflows.rbs +13 -0
  184. data/sig/didit/resources.rbs +50 -0
  185. data/sig/didit/types/account.rbs +22 -0
  186. data/sig/didit/types/account_components.rbs +94 -0
  187. data/sig/didit/types/address_validation.rbs +7 -0
  188. data/sig/didit/types/age_estimation.rbs +80 -0
  189. data/sig/didit/types/aml.rbs +174 -0
  190. data/sig/didit/types/aml_monitoring.rbs +67 -0
  191. data/sig/didit/types/analytics.rbs +57 -0
  192. data/sig/didit/types/bank.rbs +9 -0
  193. data/sig/didit/types/billing.rbs +19 -0
  194. data/sig/didit/types/biometric_templates.rbs +268 -0
  195. data/sig/didit/types/blocklist.rbs +33 -0
  196. data/sig/didit/types/businesses.rbs +54 -0
  197. data/sig/didit/types/cases.rbs +414 -0
  198. data/sig/didit/types/components.rbs +10920 -0
  199. data/sig/didit/types/customization.rbs +7 -0
  200. data/sig/didit/types/database_validation.rbs +152 -0
  201. data/sig/didit/types/document_ai.rbs +123 -0
  202. data/sig/didit/types/email.rbs +112 -0
  203. data/sig/didit/types/face_match.rbs +86 -0
  204. data/sig/didit/types/face_search.rbs +112 -0
  205. data/sig/didit/types/fraud_checks.rbs +9 -0
  206. data/sig/didit/types/id_verification.rbs +196 -0
  207. data/sig/didit/types/id_wallets.rbs +55 -0
  208. data/sig/didit/types/investigator.rbs +212 -0
  209. data/sig/didit/types/ip.rbs +7 -0
  210. data/sig/didit/types/kyb.rbs +1254 -0
  211. data/sig/didit/types/lists.rbs +106 -0
  212. data/sig/didit/types/monitoring_events.rbs +234 -0
  213. data/sig/didit/types/networks.rbs +113 -0
  214. data/sig/didit/types/passive_liveness.rbs +87 -0
  215. data/sig/didit/types/phone.rbs +65 -0
  216. data/sig/didit/types/proof_of_address.rbs +144 -0
  217. data/sig/didit/types/questionnaires.rbs +30 -0
  218. data/sig/didit/types/regulatory_reports.rbs +101 -0
  219. data/sig/didit/types/report_templates.rbs +42 -0
  220. data/sig/didit/types/sessions.rbs +1349 -0
  221. data/sig/didit/types/transactions.rbs +273 -0
  222. data/sig/didit/types/travel_rule.rbs +188 -0
  223. data/sig/didit/types/users.rbs +181 -0
  224. data/sig/didit/types/wallet_screening.rbs +40 -0
  225. data/sig/didit/types/webhooks.rbs +311 -0
  226. data/sig/didit/types/workflows.rbs +30 -0
  227. data/sig/didit/webhooks/canonical_json.rbs +25 -0
  228. data/sig/didit/webhooks/client_methods.rbs +11 -0
  229. data/sig/didit/webhooks/events.rbs +125 -0
  230. data/sig/didit/webhooks/headers.rbs +22 -0
  231. data/sig/didit/webhooks/verifier.rbs +54 -0
  232. data/sig/didit/webhooks.rbs +37 -0
  233. data/sig/didit.rbs +20 -0
  234. 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