Module: ForceDream::Canonical

Defined in:
lib/force_dream/canonical.rb

Overview

Exact replica of the server's wfCanonical: JSON.stringify(obj, Object.keys(obj).sort()). Sorted keys, no whitespace. Ported from the same logic already proven in eight other language SDKs tonight (JS, Python, Go, Rust, Java, C#, PHP, Kotlin) -- not invented fresh for Ruby.

Class Method Summary collapse

Class Method Details

.escape(str) ⇒ Object



39
40
41
# File 'lib/force_dream/canonical.rb', line 39

def escape(str)
  str.gsub('\\', '\\\\\\\\').gsub('"', '\\"').gsub("\n", '\\n').gsub("\r", '\\r').gsub("\t", '\\t')
end

.js_number(d) ⇒ Object

Mirrors JS's Number(x) -> JSON.stringify() behavior: whole values with no decimal point, fractional values preserved, never scientific notation. Confirmed directly (see module comment above) that Ruby's default #to_json needed this same correction, matching the defensive posture used even for languages where the default behavior turned out to already be safe on the cases tested.



48
49
50
51
52
53
54
55
56
57
# File 'lib/force_dream/canonical.rb', line 48

def js_number(d)
  if d.finite? && d == d.to_i && d.abs < 1e15
    d.to_i.to_s
  else
    # Ruby's Float#to_s is confirmed (via direct test) to avoid scientific notation and
    # match JS's shortest-round-trip representation for the real fractional values this
    # SDK actually deals with (pence amounts, sub-second timestamp fractions).
    d.to_s
  end
end

.serialize(value) ⇒ Object



28
29
30
31
32
33
34
35
36
37
# File 'lib/force_dream/canonical.rb', line 28

def serialize(value)
  case value
  when nil then 'null'
  when String then %("#{escape(value)}")
  when Numeric then js_number(value.to_f)
  when true, false then value.to_s
  else
    raise ArgumentError, "Unsupported type for canonicalization: #{value.class}"
  end
end

.sha256_hex(str) ⇒ Object



59
60
61
# File 'lib/force_dream/canonical.rb', line 59

def sha256_hex(str)
  Digest::SHA256.hexdigest(str)
end

.wf_canonical(obj) ⇒ Object

Uses a custom, minimal serializer rather than Ruby's own #to_json, since exact byte-for-byte output matters here (a single differing byte changes the signed bytes and breaks every signature check). Confirmed directly (not assumed) before writing this: Ruby's #to_json always includes a decimal point for Float values, even whole ones (1783860125.0, not 1783860125) -- a real, different-shaped version of the same class of bug every other language SDK tonight had to fix in its own way.



20
21
22
23
24
25
26
# File 'lib/force_dream/canonical.rb', line 20

def wf_canonical(obj)
  sorted_keys = obj.keys.sort
  parts = sorted_keys.map do |k|
    %("#{escape(k)}":#{serialize(obj[k])})
  end
  "{#{parts.join(',')}}"
end