opentrust 0.2.0 → 0.3.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 +4 -4
- data/README.md +14 -1
- data/lib/opentrust/client.rb +28 -7
- data/lib/opentrust/errors.rb +3 -0
- data/lib/opentrust/phone.rb +48 -0
- data/lib/opentrust/version.rb +1 -1
- data/lib/opentrust.rb +1 -0
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 810e3839ac5fe409bc140e8bf26e34bafaf795ee9f3cc5601f64d616c5774b60
|
|
4
|
+
data.tar.gz: 5c828d92c69579e6e191629ef860d0404179dd410f20b8c9f9aba13330c972c0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 55bbf1b388e709db9e4d7676cd47eebdef55e06a2d9b0a0ab81beabd788b7a424f70e8b899c6e5a8f142f76e247d686eb06d71c918d921554e94a8b39b424cf9
|
|
7
|
+
data.tar.gz: 765c55288178d3c0780c981b8042e469fcbb0eef305c6e0d8a1319829c2f5098191a8bca61f46ff5e690a770d1be0f6b0a5ad57128750ef21300e73993492e68
|
data/README.md
CHANGED
|
@@ -21,7 +21,7 @@ result.notice # keep this in front of anyone who sees the result
|
|
|
21
21
|
result.checks_remaining
|
|
22
22
|
|
|
23
23
|
client.lookup_many(%w[9876543210 9000000001]) # up to 100, all or nothing
|
|
24
|
-
client.report(phone: "9876543210", reason_code: "
|
|
24
|
+
client.report(phone: "9876543210", reason_code: "chargeback", notes: "Disputed 3 card payments in March", incident_at: Date.new(2026, 3, 14),
|
|
25
25
|
confirmed_by: "bank", evidence_ref: "your-ticket-123") # optional: who confirmed it (shown to others), and a private reference
|
|
26
26
|
|
|
27
27
|
# up to 100 at once; one bad row never stops the rest
|
|
@@ -33,6 +33,19 @@ client.reason_codes # => [#<struct code="cha
|
|
|
33
33
|
client.me # => { "checks_remaining" => ..., ... }
|
|
34
34
|
```
|
|
35
35
|
|
|
36
|
+
## Send hashes instead of phone numbers
|
|
37
|
+
|
|
38
|
+
Pass `hash_numbers: true` and the library sends the SHA-256 of each number instead of the number, so OpenTrust never receives it. It also sends `phone_length`, the length of the text it hashed (always 13), which OpenTrust checks to catch a hash of a differently written number. A report also sends the last four digits, so merchants and admins have something to recognise it by.
|
|
39
|
+
|
|
40
|
+
```ruby
|
|
41
|
+
client = OpenTrust::Client.new(hash_numbers: true)
|
|
42
|
+
client.lookup("98765 43210") # sends only the hash and its length (always 13)
|
|
43
|
+
OpenTrust.hash_phone("98765 43210") # => "f3a47ce5ce3d4ca8ad15225a245b2759022f79489f5c62719b8c9490f7aab90e"
|
|
44
|
+
client.verify_hashing("98765 43210") # development aid: asks OpenTrust to compare (sends the number: use a test number)
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
The format is exact, and a hash computed from a differently written number just finds nothing, so use the helper instead of hashing by hand. It is tested against the shared [test vectors](https://github.com/parity/opentrust/blob/main/sdks/phone-hash-vectors.json). A hash of a phone number can in principle be reversed by trying every possible number: this limits what is sent and stored, it does not make a number anonymous.
|
|
48
|
+
|
|
36
49
|
## Please don't block checkout on OpenTrust
|
|
37
50
|
|
|
38
51
|
Results are unverified observations from individual merchants. Use them as one input, have a person review decisions that matter, and fail open if OpenTrust is unreachable:
|
data/lib/opentrust/client.rb
CHANGED
|
@@ -9,7 +9,8 @@ module OpenTrust
|
|
|
9
9
|
# not a finding about anyone. Use them as one input among others.
|
|
10
10
|
#
|
|
11
11
|
# `reports` and `report_count` only hold OTHER merchants' reports. `own_report` says whether you have shared this number yourself.
|
|
12
|
-
|
|
12
|
+
# `phone_sha256` is set when the number was sent as a hash (then `phone_last4` is empty: we never saw the number).
|
|
13
|
+
Lookup = Struct.new(:phone_last4, :phone_sha256, :report_count, :reports, :own_report, :notice, :checks_remaining, keyword_init: true) do
|
|
13
14
|
def reported?
|
|
14
15
|
report_count.to_i.positive?
|
|
15
16
|
end
|
|
@@ -42,24 +43,29 @@ module OpenTrust
|
|
|
42
43
|
class Client
|
|
43
44
|
DEFAULT_BASE_URL = "https://opentrust.in/api/v1".freeze
|
|
44
45
|
|
|
46
|
+
# hash_numbers: send the SHA-256 of each number (see OpenTrust::Phone) instead of the number, so OpenTrust never receives it.
|
|
47
|
+
# A hash can in principle be reversed by brute force, so this limits what is sent; it does not make a number anonymous.
|
|
45
48
|
# adapter: optional callable (method, url, headers, body) -> [status, body_string], mainly for tests.
|
|
46
|
-
def initialize(api_key: ENV["OPENTRUST_API_KEY"], base_url: ENV.fetch("OPENTRUST_BASE_URL", DEFAULT_BASE_URL), timeout: 5, adapter: nil)
|
|
49
|
+
def initialize(api_key: ENV["OPENTRUST_API_KEY"], base_url: ENV.fetch("OPENTRUST_BASE_URL", DEFAULT_BASE_URL), timeout: 5, adapter: nil, hash_numbers: false)
|
|
47
50
|
raise ArgumentError, "api_key is required (or set OPENTRUST_API_KEY)" if api_key.to_s.empty?
|
|
48
51
|
|
|
49
52
|
@api_key = api_key
|
|
50
53
|
@base_url = base_url.chomp("/")
|
|
51
54
|
@timeout = timeout
|
|
52
55
|
@adapter = adapter
|
|
56
|
+
@hash_numbers = hash_numbers
|
|
53
57
|
end
|
|
54
58
|
|
|
55
59
|
# Check one number. Uses one check from your allowance.
|
|
56
60
|
def lookup(phone)
|
|
57
|
-
lookup_from(request(:post, "/lookups", phone
|
|
61
|
+
lookup_from(request(:post, "/lookups", number_fields(phone)))
|
|
58
62
|
end
|
|
59
63
|
|
|
60
64
|
# Check up to 100 numbers. All or nothing: if you don't have enough checks, none are used.
|
|
61
65
|
def lookup_many(phones)
|
|
62
|
-
|
|
66
|
+
list = Array(phones)
|
|
67
|
+
payload = @hash_numbers ? { phone_sha256s: list.map { |p| Phone.sha256(p) }, phone_length: Phone::HASHED_LENGTH } : { phones: list }
|
|
68
|
+
body = request(:post, "/lookups/batch", payload)
|
|
63
69
|
body.fetch("results").map { |r| lookup_from(r.merge("notice" => body["notice"], "checks_remaining" => body["checks_remaining"])) }
|
|
64
70
|
end
|
|
65
71
|
|
|
@@ -88,7 +94,7 @@ module OpenTrust
|
|
|
88
94
|
|
|
89
95
|
# Withdraw your own report for a number. Raises NotFoundError if you have no active report for it.
|
|
90
96
|
def withdraw_report(phone)
|
|
91
|
-
body = request(:delete, "/reports", phone
|
|
97
|
+
body = request(:delete, "/reports", number_fields(phone))
|
|
92
98
|
Withdrawal.new(phone_last4: body["phone_last4"], status: body["status"])
|
|
93
99
|
end
|
|
94
100
|
|
|
@@ -102,16 +108,31 @@ module OpenTrust
|
|
|
102
108
|
request(:get, "/me")
|
|
103
109
|
end
|
|
104
110
|
|
|
111
|
+
# A development aid: asks OpenTrust whether it computes the same SHA-256 as this library for a number. Use a test number, not a
|
|
112
|
+
# customer's: it sends the number. Returns a Hash with "match" (true or false). It costs no checks.
|
|
113
|
+
def verify_hashing(test_phone)
|
|
114
|
+
request(:post, "/phone_hash/check", phone: test_phone, phone_sha256: Phone.sha256(test_phone))
|
|
115
|
+
end
|
|
116
|
+
|
|
105
117
|
private
|
|
106
118
|
|
|
107
119
|
def report_payload(phone:, reason_code:, notes: nil, incident_at: nil, confirmed_by: nil, evidence_ref: nil)
|
|
108
120
|
incident_at = incident_at.iso8601 if incident_at.respond_to?(:iso8601)
|
|
109
|
-
|
|
121
|
+
number_fields(phone, with_last4: true).merge(reason_code: reason_code, notes: notes, incident_at: incident_at, confirmed_by: confirmed_by, evidence_ref: evidence_ref).compact
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# The number, or (with hash_numbers) its SHA-256 and, for a report, its last four digits.
|
|
125
|
+
def number_fields(phone, with_last4: false)
|
|
126
|
+
return { phone: phone } unless @hash_numbers
|
|
127
|
+
|
|
128
|
+
fields = { phone_sha256: Phone.sha256(phone), phone_length: Phone.hashed_length(phone) }
|
|
129
|
+
fields[:phone_last4] = Phone.last4(phone) if with_last4
|
|
130
|
+
fields
|
|
110
131
|
end
|
|
111
132
|
|
|
112
133
|
def lookup_from(h)
|
|
113
134
|
Lookup.new(
|
|
114
|
-
phone_last4: h["phone_last4"], report_count: h["report_count"], own_report: h["own_report"], notice: h["notice"], checks_remaining: h["checks_remaining"],
|
|
135
|
+
phone_last4: h["phone_last4"], phone_sha256: h["phone_sha256"], report_count: h["report_count"], own_report: h["own_report"], notice: h["notice"], checks_remaining: h["checks_remaining"],
|
|
115
136
|
reports: Array(h["reports"]).map do |r|
|
|
116
137
|
Report.new(reason_code: r["reason_code"], reason: r["reason"], notes: r["notes"], incident_at: r["incident_at"], confirmed_by: r["confirmed_by"], reported_at: r["reported_at"], reporter_verified: r["reporter_verified"])
|
|
117
138
|
end
|
data/lib/opentrust/errors.rb
CHANGED
|
@@ -10,6 +10,9 @@ module OpenTrust
|
|
|
10
10
|
end
|
|
11
11
|
end
|
|
12
12
|
|
|
13
|
+
# Raised before anything is sent: the number is not an Indian mobile number, so it cannot be checked or hashed.
|
|
14
|
+
class InvalidPhoneError < ArgumentError; end
|
|
15
|
+
|
|
13
16
|
class ConnectionError < Error; end # network failure or timeout
|
|
14
17
|
class AuthenticationError < Error; end # 401: missing or invalid API key
|
|
15
18
|
class NoChecksError < Error; end # 402: allowance used up
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
require "digest"
|
|
2
|
+
|
|
3
|
+
module OpenTrust
|
|
4
|
+
# The one place the number format is defined. OpenTrust accepts either a phone number or its SHA-256, and a hash only matches
|
|
5
|
+
# if it is computed exactly like this:
|
|
6
|
+
#
|
|
7
|
+
# 1. keep the digits, drop a leading 91 (12 digits) or 0 (11 digits), leaving 10 digits starting 6, 7, 8 or 9
|
|
8
|
+
# 2. prefix "+91", for example "+919876543210"
|
|
9
|
+
# 3. SHA-256 of that text, as 64 lowercase hex characters
|
|
10
|
+
#
|
|
11
|
+
# A hash that does not match simply finds nothing, so use this helper (and the shared test vectors) rather than hashing by hand.
|
|
12
|
+
module Phone
|
|
13
|
+
HASHED_LENGTH = 13 # +91 and ten digits
|
|
14
|
+
|
|
15
|
+
module_function
|
|
16
|
+
|
|
17
|
+
# "+91" followed by the ten digits. Raises InvalidPhoneError for anything that is not an Indian mobile number.
|
|
18
|
+
def normalize(raw)
|
|
19
|
+
digits = raw.to_s.gsub(/\D/, "")
|
|
20
|
+
digits = digits[2..-1] if digits.length == 12 && digits.start_with?("91")
|
|
21
|
+
digits = digits[1..-1] if digits.length == 11 && digits.start_with?("0")
|
|
22
|
+
raise InvalidPhoneError, "not a valid Indian mobile number" unless digits.match?(/\A[6-9]\d{9}\z/)
|
|
23
|
+
|
|
24
|
+
"+91#{digits}"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# The 64 lowercase hex characters OpenTrust expects as phone_sha256.
|
|
28
|
+
def sha256(raw)
|
|
29
|
+
Digest::SHA256.hexdigest(normalize(raw))
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# The length of the text that is hashed (13), sent as phone_length. OpenTrust refuses any other value, which catches hashes of a
|
|
33
|
+
# differently written number (10 = just the digits, 11 = leading 0, 12 = 91 without the +).
|
|
34
|
+
def hashed_length(raw)
|
|
35
|
+
normalize(raw).length
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# The last four digits, sent with a hashed report so merchants and admins have something to recognise it by.
|
|
39
|
+
def last4(raw)
|
|
40
|
+
normalize(raw)[-4..-1]
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Hash a number the way OpenTrust expects. Same as Phone.sha256.
|
|
45
|
+
def self.hash_phone(raw)
|
|
46
|
+
Phone.sha256(raw)
|
|
47
|
+
end
|
|
48
|
+
end
|
data/lib/opentrust/version.rb
CHANGED
data/lib/opentrust.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: opentrust
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- ParityCube
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-10-
|
|
11
|
+
date: 2026-10-14 00:00:00.000000000 Z
|
|
12
12
|
dependencies: []
|
|
13
13
|
description: Check mobile numbers against experiences shared by other Indian merchants,
|
|
14
14
|
and share your own. A small, dependency-free client for https://opentrust.in.
|
|
@@ -22,6 +22,7 @@ files:
|
|
|
22
22
|
- lib/opentrust.rb
|
|
23
23
|
- lib/opentrust/client.rb
|
|
24
24
|
- lib/opentrust/errors.rb
|
|
25
|
+
- lib/opentrust/phone.rb
|
|
25
26
|
- lib/opentrust/version.rb
|
|
26
27
|
homepage: https://opentrust.in/docs
|
|
27
28
|
licenses:
|