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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Didit
4
+ VERSION = "1.0.0" # x-release-please-version
5
+ end
@@ -0,0 +1,187 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Didit
4
+ module Webhooks
5
+ # Canonical JSON, the text +X-Signature-V2+ signs (docs/DESIGN.md section 9.2).
6
+ #
7
+ # It is byte for byte what the Didit backend builds in Python:
8
+ # +json.dumps(shorten_floats(value), sort_keys=True, separators=(",", ":"), ensure_ascii=False)+.
9
+ #
10
+ # @api private
11
+ module CanonicalJson
12
+ # Deepest nesting of objects and arrays accepted, the default of Ruby's JSON parser.
13
+ MAX_NESTING = 100
14
+
15
+ # What Python's json.dumps escapes in a string; everything else is written as literal UTF-8.
16
+ ESCAPES = (0x00..0x1f).to_h { |code| [code.chr(Encoding::UTF_8), format("\\u%04x", code)] }.merge(
17
+ "\"" => "\\\"", "\\" => "\\\\", "\n" => "\\n", "\r" => "\\r", "\t" => "\\t", "\b" => "\\b", "\f" => "\\f"
18
+ ).freeze
19
+ ESCAPED = /["\\\x00-\x1f]/
20
+
21
+ module_function
22
+
23
+ # The canonical JSON text of a parsed JSON value (keys may be Strings or Symbols).
24
+ #
25
+ # @raise [ArgumentError] when the value is not JSON data (NaN, Infinity, a string that is
26
+ # not valid UTF-8 or holds a lone surrogate, nesting deeper than {MAX_NESTING}, another type)
27
+ def generate(value)
28
+ dump(normalize(value))
29
+ end
30
+
31
+ # A deep copy of a parsed JSON value with String keys and UTF-8 strings, checked to be
32
+ # JSON data that Python's json.dumps can write.
33
+ #
34
+ # @raise [ArgumentError] see {generate}
35
+ def normalize(value, depth = 0)
36
+ case value
37
+ when Hash then normalize_object(value, depth)
38
+ when Array then normalize_array(value, depth)
39
+ when String then utf8(value)
40
+ when Float
41
+ raise ArgumentError, "NaN and Infinity are not JSON numbers" unless value.finite?
42
+
43
+ value
44
+ when Integer, true, false, nil then value
45
+ else raise ArgumentError, "a #{value.class} is not JSON data"
46
+ end
47
+ end
48
+
49
+ # The canonical text of a value {normalize} returned.
50
+ def dump(value)
51
+ out = +""
52
+ write(value, out)
53
+ out
54
+ end
55
+
56
+ # Python's +str()+ of a parsed JSON scalar, as the backend formats the fields
57
+ # +X-Signature-Simple+ signs; nil for an object or an array.
58
+ def python_str(value)
59
+ case value
60
+ when nil then "None"
61
+ when true then "True"
62
+ when false then "False"
63
+ when String then value
64
+ when Integer then value.to_s
65
+ when Float then float_repr(value)
66
+ end
67
+ end
68
+
69
+ # Python's +repr()+ of a finite float: the shortest digits that round-trip (the digits
70
+ # Ruby's Float#to_s picks too), in scientific notation when the decimal exponent is
71
+ # below -4 or at least 16 (+1e-05+, +1.5e+16+), else in fixed notation (+0.0001+, +87.0+).
72
+ def float_repr(value)
73
+ return value.to_s if value.zero? # "0.0" or "-0.0", like Python
74
+
75
+ text = value.to_s
76
+ sign = text.start_with?("-") ? "-" : ""
77
+ digits, point = decimal_digits(text.delete_prefix("-"))
78
+ "#{sign}#{format_repr(digits, point)}"
79
+ end
80
+
81
+ # The significant digits of a positive Float#to_s and the position of the decimal point
82
+ # relative to them: the value is 0.<digits> * 10**point.
83
+ def decimal_digits(text)
84
+ mantissa, exponent = text.split("e", 2)
85
+ int, frac = mantissa.to_s.split(".", 2)
86
+ int = int.to_s
87
+ frac = frac.to_s
88
+ point = int.length + (exponent ? Integer(exponent, 10) : 0)
89
+ digits = "#{int}#{frac}"
90
+ stripped = digits.sub(/\A0+/, "")
91
+ point -= digits.length - stripped.length
92
+ [stripped.sub(/0+\z/, ""), point]
93
+ end
94
+
95
+ def format_repr(digits, point)
96
+ if point <= -4 || point > 16
97
+ exponent = point - 1
98
+ mantissa = digits.length > 1 ? "#{digits[0]}.#{digits[1..]}" : digits
99
+ "#{mantissa}e#{exponent.negative? ? "-" : "+"}#{exponent.abs.to_s.rjust(2, "0")}"
100
+ elsif point <= 0
101
+ "0.#{"0" * -point}#{digits}"
102
+ elsif point >= digits.length
103
+ "#{digits}#{"0" * (point - digits.length)}.0"
104
+ else
105
+ "#{digits[0, point]}.#{digits[point..]}"
106
+ end
107
+ end
108
+
109
+ def normalize_object(value, depth)
110
+ raise ArgumentError, "JSON nesting is deeper than #{MAX_NESTING}" if depth >= MAX_NESTING
111
+
112
+ # @type var out: Hash[String, untyped]
113
+ out = {}
114
+ value.each do |key, item|
115
+ name = key_name(key)
116
+ raise ArgumentError, "the object has the key #{name.inspect} twice" if out.key?(name)
117
+
118
+ out[name] = normalize(item, depth + 1)
119
+ end
120
+ out
121
+ end
122
+
123
+ def normalize_array(value, depth)
124
+ raise ArgumentError, "JSON nesting is deeper than #{MAX_NESTING}" if depth >= MAX_NESTING
125
+
126
+ value.map { |item| normalize(item, depth + 1) }
127
+ end
128
+
129
+ def key_name(key)
130
+ case key
131
+ when String then utf8(key)
132
+ when Symbol then utf8(key.name)
133
+ else raise ArgumentError, "an object key must be a String or a Symbol, not a #{key.class}"
134
+ end
135
+ end
136
+
137
+ def utf8(value)
138
+ text =
139
+ case value.encoding
140
+ when Encoding::UTF_8 then value
141
+ when Encoding::BINARY, Encoding::US_ASCII then value.dup.force_encoding(Encoding::UTF_8)
142
+ else value.encode(Encoding::UTF_8)
143
+ end
144
+ raise ArgumentError, "a string is not valid UTF-8 or holds a lone surrogate" unless text.valid_encoding?
145
+
146
+ text
147
+ rescue EncodingError
148
+ raise ArgumentError, "a string cannot be converted to UTF-8"
149
+ end
150
+
151
+ def write(value, out)
152
+ case value
153
+ when Hash then write_object(value, out)
154
+ when Array then write_array(value, out)
155
+ when String then write_string(value, out)
156
+ when Float then out << (value.modulo(1).zero? ? value.to_i.to_s : float_repr(value))
157
+ when nil then out << "null"
158
+ else out << value.to_s # Integer, true, false
159
+ end
160
+ end
161
+
162
+ def write_object(value, out)
163
+ out << "{"
164
+ value.keys.sort!.each_with_index do |key, index|
165
+ out << "," if index.positive?
166
+ write_string(key, out)
167
+ out << ":"
168
+ write(value[key], out)
169
+ end
170
+ out << "}"
171
+ end
172
+
173
+ def write_array(value, out)
174
+ out << "["
175
+ value.each_with_index do |item, index|
176
+ out << "," if index.positive?
177
+ write(item, out)
178
+ end
179
+ out << "]"
180
+ end
181
+
182
+ def write_string(value, out)
183
+ out << "\"" << value.gsub(ESCAPED, ESCAPES) << "\""
184
+ end
185
+ end
186
+ end
187
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Didit
4
+ module Webhooks
5
+ # Adds {Didit::Webhooks.verify} and {Didit::Webhooks.verify_parsed} to +client.webhooks+,
6
+ # next to +client.webhooks.destinations+. They need no API key and send no request.
7
+ module ClientMethods
8
+ # See {Didit::Webhooks.verify}.
9
+ #
10
+ # @return [Event]
11
+ def verify(raw_body, headers, secret, tolerance: DEFAULT_TOLERANCE, allow_simple: false, now: nil)
12
+ Webhooks.verify(raw_body, headers, secret, tolerance: tolerance, allow_simple: allow_simple, now: now)
13
+ end
14
+
15
+ # See {Didit::Webhooks.verify_parsed}.
16
+ #
17
+ # @return [Event]
18
+ def verify_parsed(payload, headers, secret, tolerance: DEFAULT_TOLERANCE, now: nil)
19
+ Webhooks.verify_parsed(payload, headers, secret, tolerance: tolerance, now: now)
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,221 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Didit
4
+ module Webhooks
5
+ # A webhook event (docs/DESIGN.md section 9.4); {Didit::Webhooks.verify} returns the subclass
6
+ # for its +webhook_type+.
7
+ #
8
+ # Every field Didit sent stays readable with {#[]} and {#to_h}, typed or not.
9
+ class Event < Didit::Model
10
+ # Unique id of the event; retries carry the same id, so use it to drop duplicates.
11
+ field :event_id, "event_id", :string
12
+ # Read by #webhook_type, which falls back to +event_type+.
13
+ fields[:webhook_type] = ["webhook_type", :string]
14
+ # Unix seconds when the delivery was signed (X-Timestamp repeats it).
15
+ field :timestamp, "timestamp", :integer
16
+ # Unix seconds when the event happened, as sent.
17
+ field :created_at, "created_at", :integer
18
+ # The Didit application the event belongs to.
19
+ field :application_id, "application_id", :string
20
+ # +"live"+ or +"sandbox"+.
21
+ field :environment, "environment", :string
22
+
23
+ # Builds the event class that matches the payload's +webhook_type+ (or +event_type+).
24
+ #
25
+ # @api private
26
+ def self.build(payload, verified_by = nil)
27
+ data = payload.transform_keys(&:to_s) # a copy: the model freezes what it wraps
28
+ type = data["webhook_type"]
29
+ type = data["event_type"] unless type.is_a?(String)
30
+ klass = type.is_a?(String) ? EVENT_CLASSES.fetch(type, UnknownEvent) : UnknownEvent
31
+ klass.new(data, verified_by: verified_by)
32
+ end
33
+
34
+ # The signature header that verified the delivery (+"X-Signature"+, +"X-Signature-V2"+,
35
+ # their +-Previous+ variants, +"X-Signature-Simple"+ or +"X-Signature-Simple-Previous"+);
36
+ # nil for an event built with {Didit::Webhooks.parse_event}.
37
+ #
38
+ # @return [String, nil]
39
+ attr_reader :verified_by
40
+
41
+ # @param data [Hash] the parsed JSON body
42
+ # @param verified_by [String, nil] the header that verified it
43
+ def initialize(data = {}, verified_by: nil)
44
+ super(data)
45
+ @verified_by = verified_by
46
+ end
47
+
48
+ # The event type, for example +"status.updated"+. Callbacks that name it +event_type+
49
+ # (+kyb.registry_search.resolved+) report that value.
50
+ #
51
+ # @return [String, nil]
52
+ def webhook_type
53
+ value = self["webhook_type"]
54
+ return value if value.is_a?(String)
55
+
56
+ fallback = self["event_type"]
57
+ fallback.is_a?(String) ? fallback : value
58
+ end
59
+
60
+ # Whether a signature covered the whole body. False when only the deprecated
61
+ # X-Signature-Simple matched (it signs timestamp, session_id, status and webhook_type,
62
+ # nothing else): re-fetch the record from the API before trusting any other field.
63
+ # Also false for an event built with {Didit::Webhooks.parse_event}.
64
+ def body_authenticated?
65
+ Verifier::FULL_BODY.include?(@verified_by)
66
+ end
67
+ end
68
+
69
+ # Fields of the session events, +status.updated+ and +data.updated+.
70
+ class SessionEvent < Event
71
+ field :session_id, "session_id", :string
72
+ # Set on business (KYB) sessions, where it equals session_id.
73
+ field :business_session_id, "business_session_id", :string
74
+ # +"user"+ or +"business"+.
75
+ field :session_kind, "session_kind", :string
76
+ field :status, "status", :string
77
+ field :workflow_id, "workflow_id", :string
78
+ field :workflow_version, "workflow_version", :any
79
+ field :vendor_data, "vendor_data", :string
80
+ field :metadata, "metadata", :any
81
+ # The session decision, the shape +sessions.retrieve_decision+ returns.
82
+ field :decision, "decision", "SessionDecision"
83
+ # The steps the user must redo and why, when status is Resubmitted.
84
+ field :resubmit_info, "resubmit_info", :any
85
+ end
86
+
87
+ # +status.updated+: a verification session changed status.
88
+ class StatusUpdatedEvent < SessionEvent
89
+ WEBHOOK_TYPE = "status.updated"
90
+ end
91
+
92
+ # +data.updated+: the data of a verification session changed.
93
+ class DataUpdatedEvent < SessionEvent
94
+ WEBHOOK_TYPE = "data.updated"
95
+ end
96
+
97
+ # Fields of the user events.
98
+ class UserEvent < Event
99
+ field :vendor_user_id, "vendor_user_id", :string
100
+ field :vendor_data, "vendor_data", :string
101
+ field :status, "status", :string
102
+ end
103
+
104
+ # +user.status.updated+: a user changed status.
105
+ class UserStatusUpdatedEvent < UserEvent
106
+ WEBHOOK_TYPE = "user.status.updated"
107
+ end
108
+
109
+ # +user.data.updated+: the data of a user changed.
110
+ class UserDataUpdatedEvent < UserEvent
111
+ WEBHOOK_TYPE = "user.data.updated"
112
+ end
113
+
114
+ # Fields of the business events.
115
+ class BusinessEvent < Event
116
+ field :vendor_business_id, "vendor_business_id", :string
117
+ field :vendor_data, "vendor_data", :string
118
+ field :status, "status", :string
119
+ end
120
+
121
+ # +business.status.updated+: a business changed status.
122
+ class BusinessStatusUpdatedEvent < BusinessEvent
123
+ WEBHOOK_TYPE = "business.status.updated"
124
+ end
125
+
126
+ # +business.data.updated+: the data of a business changed.
127
+ class BusinessDataUpdatedEvent < BusinessEvent
128
+ WEBHOOK_TYPE = "business.data.updated"
129
+ end
130
+
131
+ # Fields of the transaction monitoring events.
132
+ class TransactionEvent < Event
133
+ # Didit's transaction id.
134
+ field :transaction_id, "transaction_id", :string
135
+ # The transaction id you sent.
136
+ field :txn_id, "txn_id", :string
137
+ field :status, "status", :string
138
+ field :score, "score", :float
139
+ field :severity, "severity", :string
140
+ # A decimal string, exactly as sent.
141
+ field :amount, "amount", :any
142
+ field :currency, "currency", :string
143
+ field :direction, "direction", :string
144
+ field :action_required, "action_required", :boolean
145
+ end
146
+
147
+ # +transaction.created+: a transaction was created.
148
+ class TransactionCreatedEvent < TransactionEvent
149
+ WEBHOOK_TYPE = "transaction.created"
150
+ end
151
+
152
+ # +transaction.status.updated+: a transaction changed status.
153
+ class TransactionStatusUpdatedEvent < TransactionEvent
154
+ WEBHOOK_TYPE = "transaction.status.updated"
155
+ end
156
+
157
+ # +travel_rule.status.updated+: the Travel Rule status of a transaction changed.
158
+ class TravelRuleStatusUpdatedEvent < Event
159
+ WEBHOOK_TYPE = "travel_rule.status.updated"
160
+
161
+ field :transaction_id, "transaction_id", :string
162
+ field :txn_id, "txn_id", :string
163
+ field :travel_rule_status, "travel_rule_status", :string
164
+ field :rail, "rail", :string
165
+ field :direction, "direction", :string
166
+ end
167
+
168
+ # +activity.created+: an activity was recorded; read other fields with #[].
169
+ class ActivityCreatedEvent < Event
170
+ WEBHOOK_TYPE = "activity.created"
171
+ end
172
+
173
+ # +workflow.compliance_changed+: the compliance state of a workflow changed.
174
+ class WorkflowComplianceChangedEvent < Event
175
+ WEBHOOK_TYPE = "workflow.compliance_changed"
176
+
177
+ field :workflow_id, "workflow_id", :string
178
+ end
179
+
180
+ # +kyb.registry_search.resolved+: an asynchronous KYB registry search (+kyb.search+ with
181
+ # +webhook_url+) finished.
182
+ #
183
+ # Didit sends this callback unsigned (header +X-Didit-Unsigned-Callback: true+, the type in
184
+ # +event_type+), so {Didit::Webhooks.verify} rejects it with +:missing_signature+. Read it with
185
+ # {Didit::Webhooks.parse_event}, treat it as untrusted, and fetch the result from the API.
186
+ class KybRegistrySearchResolvedEvent < Event
187
+ WEBHOOK_TYPE = "kyb.registry_search.resolved"
188
+
189
+ # The search request id +kyb.search+ returned.
190
+ field :request_id, "request_id", :string
191
+ field :vendor_data, "vendor_data", :string
192
+ field :metadata, "metadata", :any
193
+ # For example +"resolved"+.
194
+ field :search_status, "search_status", :string
195
+ field :search_resolved, "search_resolved", :boolean
196
+ # The registry result with the candidate companies.
197
+ field :kyb_registry, "kyb_registry", :any
198
+ end
199
+
200
+ # An event type this version of the SDK does not know; the whole payload stays readable
201
+ # with #[] and #to_h.
202
+ class UnknownEvent < Event
203
+ end
204
+
205
+ # The event class of each +webhook_type+; any other type is an {UnknownEvent}.
206
+ EVENT_CLASSES = {
207
+ StatusUpdatedEvent::WEBHOOK_TYPE => StatusUpdatedEvent,
208
+ DataUpdatedEvent::WEBHOOK_TYPE => DataUpdatedEvent,
209
+ UserStatusUpdatedEvent::WEBHOOK_TYPE => UserStatusUpdatedEvent,
210
+ UserDataUpdatedEvent::WEBHOOK_TYPE => UserDataUpdatedEvent,
211
+ BusinessStatusUpdatedEvent::WEBHOOK_TYPE => BusinessStatusUpdatedEvent,
212
+ BusinessDataUpdatedEvent::WEBHOOK_TYPE => BusinessDataUpdatedEvent,
213
+ ActivityCreatedEvent::WEBHOOK_TYPE => ActivityCreatedEvent,
214
+ TransactionCreatedEvent::WEBHOOK_TYPE => TransactionCreatedEvent,
215
+ TransactionStatusUpdatedEvent::WEBHOOK_TYPE => TransactionStatusUpdatedEvent,
216
+ TravelRuleStatusUpdatedEvent::WEBHOOK_TYPE => TravelRuleStatusUpdatedEvent,
217
+ WorkflowComplianceChangedEvent::WEBHOOK_TYPE => WorkflowComplianceChangedEvent,
218
+ KybRegistrySearchResolvedEvent::WEBHOOK_TYPE => KybRegistrySearchResolvedEvent
219
+ }.freeze
220
+ end
221
+ end
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Didit
4
+ module Webhooks
5
+ # The webhook headers of a request, read from whatever the framework has.
6
+ #
7
+ # Accepts a Hash with header names in any case, a Rack env (+HTTP_X_SIGNATURE_V2+, which is
8
+ # also what Rails +request.headers+ and Sinatra +request.env+ yield), or any object whose
9
+ # +each+ yields +[name, value]+ pairs (Net::HTTPHeader, an Array of pairs). Values may be
10
+ # Strings or Arrays of Strings. Empty values count as absent.
11
+ #
12
+ # @api private
13
+ class Headers
14
+ # Header names (lower case) the verifier reads.
15
+ WANTED = [
16
+ HEADER_SIGNATURE, HEADER_SIGNATURE_V2, HEADER_SIGNATURE_SIMPLE,
17
+ HEADER_SIGNATURE_PREVIOUS, HEADER_SIGNATURE_V2_PREVIOUS, HEADER_SIGNATURE_SIMPLE_PREVIOUS,
18
+ HEADER_TIMESTAMP
19
+ ].map(&:downcase).freeze
20
+
21
+ # A well-formed signature: 64 lowercase hex characters.
22
+ HEX = /\A[0-9a-f]{64}\z/
23
+
24
+ # @raise [Didit::ConfigurationError] when +source+ cannot be enumerated
25
+ def initialize(source)
26
+ unless source.respond_to?(:each)
27
+ raise ConfigurationError,
28
+ "headers must be a Hash, a Rack env or [name, value] pairs, not #{source.class}."
29
+ end
30
+
31
+ @values = {}
32
+ source.each { |name, value| add(name, value) }
33
+ end
34
+
35
+ # Whether the header has a non-empty value.
36
+ def present?(name)
37
+ !parts(name).empty?
38
+ end
39
+
40
+ # The well-formed signatures a header carries. A proxy may have joined repeated
41
+ # headers with commas, so every comma-separated part counts.
42
+ def signatures(name)
43
+ parts(name).grep(HEX)
44
+ end
45
+
46
+ # The first non-empty value of +X-Timestamp+, stripped.
47
+ def timestamp
48
+ values(HEADER_TIMESTAMP).first
49
+ end
50
+
51
+ private
52
+
53
+ def add(name, value)
54
+ key = header_key(name)
55
+ return unless key && WANTED.include?(key)
56
+
57
+ (value.is_a?(Array) ? value : [value]).each do |item|
58
+ item = item.to_s if item.is_a?(Integer)
59
+ next unless item.is_a?(String)
60
+
61
+ text = item.b.strip
62
+ (@values[key] ||= []) << text unless text.empty?
63
+ end
64
+ end
65
+
66
+ def header_key(name)
67
+ name = name.name if name.is_a?(Symbol)
68
+ return unless name.is_a?(String)
69
+
70
+ key = name.b.strip.downcase
71
+ key.start_with?("http_") ? key.delete_prefix("http_").tr("_", "-") : key
72
+ end
73
+
74
+ def values(name)
75
+ @values.fetch(name.downcase, [])
76
+ end
77
+
78
+ def parts(name)
79
+ values(name).flat_map { |value| value.split(",").map(&:strip) }.reject(&:empty?)
80
+ end
81
+ end
82
+ end
83
+ end