zeroclick-sellers 0.6.0 → 0.7.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: 6127c1b9413ac6cbf8d9690e972e319418e2bee6ba3a025ba057c6752d04a370
4
- data.tar.gz: 384bfdd1ba3393497943aa9c37138f0b9fae69098a2404b3dfd6b2e51f61493b
3
+ metadata.gz: 27bd4f5d8f10f026b12a89ee8681fef45ffbf58129df180f40903a55ec9743d3
4
+ data.tar.gz: 1ea73b94f08215d28c2974ac4937bdc423aa2f52a66c688a5746656ad837c8f2
5
5
  SHA512:
6
- metadata.gz: 446af6c8cb1563130c222f09ec8ee8eeb4308636782925acd269d7353a1ed3cbc52f7aa8ac033fb0a1eb6bb4fe268b2d5178a5b483e7e7cf9348c18ea291a99d
7
- data.tar.gz: 40f965870f70c61902865c586d930c39f4c1165a943244dfc6647d05abdbc93bb2faf65f184188cc80e03e45a860b5c410c5db8d2c97f48545206a5a663657cc
6
+ metadata.gz: 73ef4fc05515a9d11212ca90c7df1c9f97c157bdc2628c4b1bb81738849552fbd7aca334986fc60f1c4d0d5cb6ca311b7663c453c1633a756950e82af4f06b42
7
+ data.tar.gz: 00dbf768601b7808759518ad6a507e0d238f4c6c65aecdd91ce5106488841a19538f5ea49ea5df93e776a049330f32f834d7d9216dd0ce58b212bf6da24e1748
@@ -191,7 +191,9 @@ module ZeroClick
191
191
  # +seller+ is the seller's public id; +path+ must be an absolute path.
192
192
  def report_page_view(seller:, path:, status: nil, duration_ms: nil, representation: nil,
193
193
  journey_id: nil, captured_at: nil, user_agent: nil, accept: nil,
194
- client_ip: nil, country: nil, referrer_host: nil)
194
+ client_ip: nil, country: nil, referrer_host: nil, host: nil,
195
+ eval_tag: nil, eval_signature: nil, eval_signature_input: nil,
196
+ eval_signature_agent: nil, source: nil, source_signature: nil)
195
197
  if @page_views_key.nil? || @page_views_key.empty?
196
198
  raise Error.new("page_views_not_configured", operation: "report_page_view")
197
199
  end
@@ -200,7 +202,10 @@ module ZeroClick
200
202
  seller: seller, path: path, status: status, duration_ms: duration_ms,
201
203
  representation: representation, journey_id: journey_id, captured_at: captured_at,
202
204
  user_agent: user_agent, accept: accept, client_ip: client_ip,
203
- country: country, referrer_host: referrer_host
205
+ country: country, referrer_host: referrer_host, host: host,
206
+ eval_tag: eval_tag, eval_signature: eval_signature,
207
+ eval_signature_input: eval_signature_input, eval_signature_agent: eval_signature_agent,
208
+ source: source, source_signature: source_signature
204
209
  )
205
210
  status_code, body = @http_post.call(
206
211
  base_url: @api_base_url,
@@ -22,6 +22,11 @@ module ZeroClick
22
22
  # - the app runs FIRST, so the beacon carries the real status and
23
23
  # duration; the visitor's user-agent, accept, and connecting IP are
24
24
  # forwarded for server-side classification;
25
+ # - a ZeroClick agent-eval request's tag and signature are forwarded
26
+ # with the page's host, so the API can keep ZeroClick's own eval
27
+ # traffic out of the seller's numbers; a browser-based run, which
28
+ # cannot sign, is told by the +src+ marker on its start URL,
29
+ # forwarded with the host and the signature beside it;
25
30
  # - ANY beacon failure — missing key, network error, non-204, timeout,
26
31
  # a bad resolve_url or clock — is handed to +on_error+ and swallowed.
27
32
  # The middleware must never break or alter the customer's site.
@@ -43,6 +48,13 @@ module ZeroClick
43
48
  # duration. Injectable so a test can make both deterministic.
44
49
  DEFAULT_CLOCK = -> { Process.clock_gettime(Process::CLOCK_REALTIME, :float_millisecond) }
45
50
 
51
+ # ZeroClick's own start marker: the one query value the middleware
52
+ # forwards. A browser-based eval run's requests leave from the agent
53
+ # vendor's cloud, out of reach of the signed tag, so agent-eval marks
54
+ # its start URL as ?src=ze-<experiment>-<n> instead, and the API keeps
55
+ # the whole visit out of the seller's numbers from it.
56
+ EVAL_SOURCE_PREFIX = "ze-"
57
+
46
58
  # +seller+ may be a client, a callable returning one, or omitted to
47
59
  # resolve the process-wide ZeroClick::Sellers.seller on the first
48
60
  # request (the Rails path — see Base#initialize for why).
@@ -51,7 +63,8 @@ module ZeroClick
51
63
  # by the wire contract). +clock+ returns epoch milliseconds.
52
64
  # +resolve_url+ is a callable taking the Rack env and returning the
53
65
  # public URL of the request, for servers behind a proxy that rewrites
54
- # the host the runtime sees. +on_error+ observes the fire-and-forget
66
+ # the host the runtime sees; its path is reported and, with an eval
67
+ # tag, its host. +on_error+ observes the fire-and-forget
55
68
  # path; it is never re-raised.
56
69
  def initialize(app, seller_id:, seller: nil, clock: nil, resolve_url: nil, on_error: nil)
57
70
  @app = app
@@ -86,7 +99,8 @@ module ZeroClick
86
99
  return unless env["REQUEST_METHOD"] == "GET"
87
100
 
88
101
  url = @resolve_url ? @resolve_url.call(env) : Agentify.public_url(env)
89
- path = URI.parse(url.to_s).path
102
+ uri = URI.parse(url.to_s)
103
+ path = uri.path
90
104
  path = "/" if path.nil? || path.empty?
91
105
  return if ASSET_PATH.match?(path)
92
106
 
@@ -102,10 +116,67 @@ module ZeroClick
102
116
  client_ip: client_ip_of(env),
103
117
  country: env["HTTP_CF_IPCOUNTRY"],
104
118
  referrer_host: referrer_host_of(env),
105
- journey_id: journey_id_of(headers)
119
+ journey_id: journey_id_of(headers),
120
+ **start_marker_of(uri),
121
+ **eval_run_of(env, uri)
106
122
  )
107
123
  end
108
124
 
125
+ # ZeroClick's own agent-eval run, when the visitor carried its tag: the
126
+ # tag, the signature agent-eval puts on a seller's site (under
127
+ # ZeroClick-only header names, never Web Bot Auth's) and the public
128
+ # host it was signed for. Forwarded as sent; the API labels the view
129
+ # only when the signature verifies, so a forged tag labels nothing. A
130
+ # value outside what the beacon accepts (empty, or longer than
131
+ # agent-eval ever sends) is left out rather than sent, so a visitor's
132
+ # odd header can never cost the page view itself.
133
+ def eval_run_of(env, uri)
134
+ tag = capped(env["HTTP_X_ZEROCLICK_EVAL"], 512)
135
+ return {} if tag.nil?
136
+
137
+ {
138
+ host: capped(host_of(uri), 300),
139
+ eval_tag: tag,
140
+ eval_signature: capped(env["HTTP_X_ZEROCLICK_EVAL_SIGNATURE"], 1_024),
141
+ eval_signature_input: capped(env["HTTP_X_ZEROCLICK_EVAL_SIGNATURE_INPUT"], 1_024),
142
+ eval_signature_agent: capped(env["HTTP_X_ZEROCLICK_EVAL_SIGNATURE_AGENT"], 512)
143
+ }
144
+ end
145
+
146
+ def capped(value, max)
147
+ value && !value.empty? && value.length <= max ? value : nil
148
+ end
149
+
150
+ # The public host the page was requested on, as the JS SDK's URL.host
151
+ # reads it: the host and any non-default port.
152
+ def host_of(uri)
153
+ return nil if uri.host.nil? || uri.host.empty?
154
+
155
+ uri.port && uri.port != uri.default_port ? "#{uri.host}:#{uri.port}" : uri.host
156
+ end
157
+
158
+ # ZeroClick's own start marker, keyed like report_page_view: empty
159
+ # without one. The +src+ query value when it is ours, the page's host
160
+ # and the +signature+ agent-eval puts beside the marker, as sent: the
161
+ # only query values the beacon carries. Any other +src+ stays on the
162
+ # site, and a marker or signature past what the beacon accepts (or a
163
+ # query that does not decode) is left out rather than sent.
164
+ def start_marker_of(uri)
165
+ return {} if uri.query.nil? || uri.query.empty?
166
+
167
+ values = URI.decode_www_form(uri.query)
168
+ src = values.assoc("src")&.last
169
+ return {} unless src&.start_with?(EVAL_SOURCE_PREFIX) && src.length <= 32
170
+
171
+ {
172
+ host: capped(host_of(uri), 300),
173
+ source: src,
174
+ source_signature: capped(values.assoc("signature")&.last, 256)
175
+ }
176
+ rescue ArgumentError
177
+ {}
178
+ end
179
+
109
180
  # What the app served for this page-content request, read off the
110
181
  # response content-type so it reflects the real representation
111
182
  # regardless of what produced it.
@@ -146,11 +217,7 @@ module ZeroClick
146
217
  rescue URI::InvalidURIError
147
218
  nil
148
219
  end
149
- host = uri&.host
150
- return nil if host.nil? || host.empty?
151
-
152
- # Host and any non-default port only, matching the JS SDK's URL.host.
153
- uri.port && uri.port != uri.default_port ? "#{host}:#{uri.port}" : host
220
+ uri && host_of(uri)
154
221
  end
155
222
 
156
223
  # Case-insensitive lookup over the app's response headers.
@@ -27,9 +27,15 @@ module ZeroClick
27
27
  # Build the wire body: camelCase string keys, every absent optional key
28
28
  # omitted. +seller+ is the seller's public id and +path+ must be an
29
29
  # absolute path, both rejected before any request leaves the process.
30
+ # +host+ and the +eval_*+ values describe ZeroClick's own agent-eval run
31
+ # when the visitor carried its tag: the public host the run signed for
32
+ # and the tag with its signature, as sent. The API labels the view as
33
+ # eval traffic only when the signature verifies.
30
34
  def build_payload(seller:, path:, status: nil, duration_ms: nil, representation: nil,
31
35
  journey_id: nil, captured_at: nil, user_agent: nil, accept: nil,
32
- client_ip: nil, country: nil, referrer_host: nil)
36
+ client_ip: nil, country: nil, referrer_host: nil, host: nil,
37
+ eval_tag: nil, eval_signature: nil, eval_signature_input: nil,
38
+ eval_signature_agent: nil, source: nil, source_signature: nil)
33
39
  Sellers.require_slug!(seller, "seller", "report_page_view")
34
40
  unless path.is_a?(String) && path.start_with?("/")
35
41
  raise Error.new("malformed_input", operation: "report_page_view",
@@ -51,6 +57,13 @@ module ZeroClick
51
57
  payload["clientIp"] = client_ip unless client_ip.nil?
52
58
  payload["country"] = country unless country.nil?
53
59
  payload["referrerHost"] = referrer_host unless referrer_host.nil?
60
+ payload["host"] = host unless host.nil?
61
+ payload["evalTag"] = eval_tag unless eval_tag.nil?
62
+ payload["evalSignature"] = eval_signature unless eval_signature.nil?
63
+ payload["evalSignatureInput"] = eval_signature_input unless eval_signature_input.nil?
64
+ payload["evalSignatureAgent"] = eval_signature_agent unless eval_signature_agent.nil?
65
+ payload["source"] = source unless source.nil?
66
+ payload["sourceSignature"] = source_signature unless source_signature.nil?
54
67
  payload
55
68
  end
56
69
 
@@ -4,6 +4,6 @@ module ZeroClick
4
4
  module Sellers
5
5
  # Bump this to publish. The workflow reads it, asks RubyGems whether this
6
6
  # version already exists, and publishes only when it does not.
7
- VERSION = "0.6.0"
7
+ VERSION = "0.7.0"
8
8
  end
9
9
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: zeroclick-sellers
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.0
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - ZeroClick