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.



79
80
81
82
83
84
85
86
87
88
89
# File 'lib/zeroclick/sellers/middleware/agentify.rb', line 79

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
# 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?
  [200, headers, [result.markdown]]
end