Module: BSV::Auth::AuthPayload

Defined in:
lib/bsv/auth/auth_payload.rb

Overview

BRC-104 binary serialisation and deserialisation of HTTP requests and responses for signing/verification.

The wire format is byte-compatible with the TS and Go SDKs, enabling cross-SDK signature verification.

Absent optional fields (nil path, nil query, nil body) are encoded as ABSENT (VarInt MAX_UINT64 = 9 bytes of 0xFF). Zero-length fields are encoded as varint(0) followed by zero bytes — a distinct encoding from absent.

All varint operations delegate to Transaction::VarInt.

Constant Summary collapse

ABSENT =

Sentinel value for absent optional fields (Bitcoin VarInt MAX_UINT64).

BSV::Transaction::VarInt::MAX_UINT64
METHODS_WITH_BODY =

HTTP methods that typically carry a request body.

%w[POST PUT PATCH DELETE].freeze

Class Method Summary collapse

Class Method Details

.deserialize_request(data) ⇒ Hash

Deserialises a binary request payload into a Hash.

Parameters:

Returns:

  • (Hash)

    with keys :request_nonce, :method, :path, :query, :headers, :body (all values may be nil for absent optional fields)



78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
# File 'lib/bsv/auth/auth_payload.rb', line 78

def deserialize_request(data)
  bin = data.b
  pos = 0

  # 32-byte request nonce
  request_nonce = bin.byteslice(pos, 32)
  pos += 32

  # Method
  method_len, consumed = BSV::Transaction::VarInt.decode(bin, pos)
  pos += consumed
  method = bin.byteslice(pos, method_len).force_encoding('UTF-8')
  pos += method_len

  # Path (optional)
  path, pos = decode_optional_string(bin, pos)

  # Query (optional)
  query, pos = decode_optional_string(bin, pos)

  # Headers
  n_headers, consumed = BSV::Transaction::VarInt.decode(bin, pos)
  pos += consumed
  headers = []
  n_headers.times do
    k_len, consumed = BSV::Transaction::VarInt.decode(bin, pos)
    pos += consumed
    k = bin.byteslice(pos, k_len).force_encoding('UTF-8')
    pos += k_len

    v_len, consumed = BSV::Transaction::VarInt.decode(bin, pos)
    pos += consumed
    v = bin.byteslice(pos, v_len).force_encoding('UTF-8')
    pos += v_len

    headers << [k, v]
  end

  # Body (optional)
  body, _pos = decode_optional_bytes(bin, pos)

  { request_nonce: request_nonce, method: method, path: path, query: query,
    headers: headers, body: body }
end

.deserialize_response(data) ⇒ Hash

Deserialises a binary response payload into a Hash.

Parameters:

Returns:

  • (Hash)

    with keys :request_id, :status, :headers, :body



161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
# File 'lib/bsv/auth/auth_payload.rb', line 161

def deserialize_response(data)
  bin = data.b
  pos = 0

  # 32-byte request ID
  request_id = bin.byteslice(pos, 32)
  pos += 32

  # Status code
  status, consumed = BSV::Transaction::VarInt.decode(bin, pos)
  pos += consumed

  # Headers
  n_headers, consumed = BSV::Transaction::VarInt.decode(bin, pos)
  pos += consumed
  headers = []
  n_headers.times do
    k_len, consumed = BSV::Transaction::VarInt.decode(bin, pos)
    pos += consumed
    k = bin.byteslice(pos, k_len).force_encoding('UTF-8')
    pos += k_len

    v_len, consumed = BSV::Transaction::VarInt.decode(bin, pos)
    pos += consumed
    v = bin.byteslice(pos, v_len).force_encoding('UTF-8')
    pos += v_len

    headers << [k, v]
  end

  # Body (optional)
  body, _pos = decode_optional_bytes(bin, pos)

  { request_id: request_id, status: status, headers: headers, body: body }
end

.serialize_request(request_nonce:, method:, path:, query:, headers:, body:) ⇒ String

Serialises an HTTP request into a binary payload for signing.

Parameters:

  • request_nonce (String)

    raw 32-byte binary string (the request nonce)

  • method (String)

    HTTP method (e.g. ‘GET’, ‘POST’)

  • path (String, nil)

    URL path (e.g. ‘/api/v1/resource’); nil encodes as ABSENT

  • query (String, nil)

    query string including leading ‘?’ (e.g. ‘?q=hello’); nil encodes as ABSENT

  • headers (Array<Array(String, String)>)

    pre-sorted, lowercased [key, value] pairs

  • body (String, nil)

    request body bytes; nil encodes as ABSENT

Returns:

  • (String)

    binary payload (ASCII-8BIT encoding)



36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
# File 'lib/bsv/auth/auth_payload.rb', line 36

def serialize_request(request_nonce:, method:, path:, query:, headers:, body:)
  buf = ''.b

  # 32-byte request nonce — written raw, no length prefix
  buf << request_nonce.b

  # Method — always present; varint(length) + UTF-8 bytes
  effective_method = method.nil? || method.empty? ? 'GET' : method
  method_bytes = effective_method.encode('UTF-8').b
  buf << BSV::Transaction::VarInt.encode(method_bytes.bytesize)
  buf << method_bytes

  # Path — optional
  buf << encode_optional_string(path)

  # Query — optional
  buf << encode_optional_string(query)

  # Headers — varint count + key/value pairs
  pairs = headers || []
  buf << BSV::Transaction::VarInt.encode(pairs.length)
  pairs.each do |k, v|
    key_bytes = k.to_s.encode('UTF-8').b
    val_bytes = v.to_s.encode('UTF-8').b
    buf << BSV::Transaction::VarInt.encode(key_bytes.bytesize)
    buf << key_bytes
    buf << BSV::Transaction::VarInt.encode(val_bytes.bytesize)
    buf << val_bytes
  end

  # Body — apply defaults for body-carrying methods, then encode optionally
  effective_body = resolve_body(method: effective_method, headers: pairs, body: body)
  buf << encode_optional_bytes(effective_body)

  buf
end

.serialize_response(request_id:, status:, headers:, body:) ⇒ String

Serialises an HTTP response into a binary payload for signing.

Parameters:

  • request_id (String)

    raw 32-byte binary string

  • status (Integer)

    HTTP status code (e.g. 200, 404)

  • headers (Array<Array(String, String)>)

    pre-sorted, lowercased [key, value] pairs

  • body (String, nil)

    response body bytes; nil or empty encodes as ABSENT

Returns:

  • (String)

    binary payload (ASCII-8BIT encoding)



130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
# File 'lib/bsv/auth/auth_payload.rb', line 130

def serialize_response(request_id:, status:, headers:, body:)
  buf = ''.b

  # 32-byte request ID — written raw
  buf << request_id.b

  # Status code as varint
  buf << BSV::Transaction::VarInt.encode(status)

  # Headers
  pairs = headers || []
  buf << BSV::Transaction::VarInt.encode(pairs.length)
  pairs.each do |k, v|
    key_bytes = k.to_s.encode('UTF-8').b
    val_bytes = v.to_s.encode('UTF-8').b
    buf << BSV::Transaction::VarInt.encode(key_bytes.bytesize)
    buf << key_bytes
    buf << BSV::Transaction::VarInt.encode(val_bytes.bytesize)
    buf << val_bytes
  end

  # Body — write length + bytes; ABSENT if nil
  buf << encode_optional_bytes(body)

  buf
end