Class: ZeroClick::Sellers::Middleware::Agentify

Inherits:
Object
  • Object
show all
Defined in:
lib/zeroclick/sellers/middleware/agentify.rb

Overview

Rack middleware that serves agent traffic agentified markdown and passes everyone else to the app untouched. Mount it in front of the marketing site, beside Meter/Identify:

use ZeroClick::Sellers::Middleware::Agentify, seller: SELLER

The contract, shared by every ZeroClick seller SDK:

- only GET requests are considered; every other method passes
through;
- detection is Agentify.wants_agent_markdown? over the request's
Accept and User-Agent headers — markdown-preferring or AI-agent
traffic matches;
- a match short-circuits with 200 text/markdown, forwarding the
agentify response's cache-control and etag so the customer's CDN
caches it too;
- ANY agentify failure — missing key, network error, non-200,
timeout — falls through to the app. The middleware must never
break the customer's site; the worst case is an agent seeing HTML.

Unlike Meter/Identify it never reads the request body — there is nothing to verify — so no buffering ceiling applies.

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(app, seller: nil, seller_id: nil, resolve_url: nil, on_error: nil) ⇒ Agentify

seller may be a client, a callable returning one, or omitted to resolve the process-wide ZeroClick::Sellers.seller on the first request (the Rails path — see Base#initialize for why).

seller_id is forwarded to the API's seller query param (its public id); needed only by multi-seller organizations. resolve_url is a callable taking the Rack env and returning the public URL to convert, for servers behind a proxy that rewrites the scheme or host the runtime sees. on_error observes the fail-open path; it is never re-raised.



42
43
44
45
46
47
48
# File 'lib/zeroclick/sellers/middleware/agentify.rb', line 42

def initialize(app, seller: nil, seller_id: nil, resolve_url: nil, on_error: nil)
  @app = app
  @seller_source = seller
  @seller_id = seller_id
  @resolve_url = resolve_url
  @on_error = on_error
end

Class Method Details

.public_url(env) ⇒ Object

The URL the agent requested, as far as this process can see it.



85
86
87
88
89
90
91
92
93
94
95
# File 'lib/zeroclick/sellers/middleware/agentify.rb', line 85

def self.public_url(env)
  scheme = env["rack.url_scheme"] || "http"
  host = env["HTTP_HOST"]
  if host.nil? || host.empty?
    host = env["SERVER_NAME"].to_s
    port = env["SERVER_PORT"]
    default_port = scheme == "https" ? "443" : "80"
    host = "#{host}:#{port}" if port && port != default_port
  end
  "#{scheme}://#{host}#{Middleware.path_and_query_from_env(env)}"
end

Instance Method Details

#call(env) ⇒ Object



50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
# File 'lib/zeroclick/sellers/middleware/agentify.rb', line 50

def call(env)
  return @app.call(env) unless env["REQUEST_METHOD"] == "GET"

  wanted = Sellers::Agentify.wants_agent_markdown?(
    accept: env["HTTP_ACCEPT"], user_agent: env["HTTP_USER_AGENT"]
  )
  return @app.call(env) unless wanted

  begin
    url = @resolve_url ? @resolve_url.call(env) : Agentify.public_url(env)
    result = seller.fetch_agentify_markdown(url, seller: @seller_id)
  rescue StandardError => e
    # Fail open — never break the site; the worst case is HTML.
    @on_error&.call(e, env)
    return @app.call(env)
  end

  headers = {
    "content-type" => result.content_type || "text/markdown; charset=utf-8",
    # The representation depends on both detection signals; without
    # this a shared cache would hand the markdown to a browser.
    "vary" => "accept, user-agent"
  }
  headers["cache-control"] = result.cache_control unless result.cache_control.nil?
  headers["etag"] = result.etag unless result.etag.nil?
  unless result.zcj_id.nil?
    # Re-expose the journey id on the agent's response so a wrapping
    # page-view beacon can stamp this view onto the same journey.
    headers["x-zcj-id"] = result.zcj_id
    headers["access-control-expose-headers"] = "x-zcj-id"
  end
  [200, headers, [result.markdown]]
end