Class: ZeroClick::Sellers::Middleware::Agentify
- Inherits:
-
Object
- Object
- ZeroClick::Sellers::Middleware::Agentify
- 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
-
.public_url(env) ⇒ Object
The URL the agent requested, as far as this process can see it.
Instance Method Summary collapse
- #call(env) ⇒ Object
-
#initialize(app, seller: nil, seller_id: nil, resolve_url: nil, on_error: nil) ⇒ Agentify
constructor
sellermay 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).
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 |