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
|
@@ -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
|