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.
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 |