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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1be8ac1901adb4eba6e169948dd6172743d816f582c73beb84a6665757cec2ab
4
- data.tar.gz: d0f5877799fb975d2f9d34601bf86103d4f5109ce1e031e6b6daf3f6b745d4c4
3
+ metadata.gz: 810e3839ac5fe409bc140e8bf26e34bafaf795ee9f3cc5601f64d616c5774b60
4
+ data.tar.gz: 5c828d92c69579e6e191629ef860d0404179dd410f20b8c9f9aba13330c972c0
5
5
  SHA512:
6
- metadata.gz: c0a3d367db5b5697fbf9de7618c6cb29d29e4ea5ddd3628f1aca17befa00812622a52e44ccbc498bbf07ca428d63467848f568e0a95098976f3436e353a9d81e
7
- data.tar.gz: b416321fcf6e6c13fc299148d668dfdab3fbae2def6bc1671594f237c8012d8b5fb604210a5e12ca174eadd98f643947d998ab72dc39c8a90938104307bed34e
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: "cod_refusal", notes: "Refused 3 COD orders in March", incident_at: Date.new(2026, 3, 14),
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:
@@ -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
- Lookup = Struct.new(:phone_last4, :report_count, :reports, :own_report, :notice, :checks_remaining, keyword_init: true) do
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: 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
- body = request(:post, "/lookups/batch", phones: Array(phones))
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: 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
- { phone: phone, reason_code: reason_code, notes: notes, incident_at: incident_at, confirmed_by: confirmed_by, evidence_ref: evidence_ref }.compact
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
@@ -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
@@ -1,3 +1,3 @@
1
1
  module OpenTrust
2
- VERSION = "0.2.0"
2
+ VERSION = "0.3.0"
3
3
  end
data/lib/opentrust.rb CHANGED
@@ -1,3 +1,4 @@
1
1
  require_relative "opentrust/version"
2
2
  require_relative "opentrust/errors"
3
+ require_relative "opentrust/phone"
3
4
  require_relative "opentrust/client"
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.2.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-12 00:00:00.000000000 Z
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: