Skip to content

Web Bot Auth and browsers

Every request an agent sends to a site is signed with Web Bot Auth, the HTTP Message Signatures profile that Cloudflare and other CDNs check. The SDK does it for you:

agent-token: eyJ0eXAiOiJhZ2VudCtqd3Qi…
signature-agent: sig1="https://api.trybage.com/v1/agents/agt_…/directory";type=jwks_uri
signature-input: sig1=("@method" "@authority" "@path" "@query" "signature-agent";key="sig1" "agent-token");created=1793600000;expires=1793600060;keyid="kX3f…";alg="ed25519";nonce="…";tag="web-bot-auth"
signature: sig1=:…:
  • signature-agent points at your agent’s key directory at Bage. It lists your registered key (and session keys, below), so any Web Bot Auth verifier can check the signature. Bage’s whole directory is at https://api.trybage.com/.well-known/http-message-signatures-directory.
  • keyid is the JWK thumbprint of the key the token is bound to. Use an Ed25519 key: Web Bot Auth verifiers outside Bage don’t accept P-256.
  • nonce is new on every request. Sites refuse a request they’ve seen before.
  • agent-token is covered when it’s sent. Without a token, the signature still says which agent you are, just not on whose behalf (agent.sign(request)); a site checks that with bage.verifySignature(request).

The first Bage format (without signature-agent, alg or a nonce) still verifies, so older agents keep working.

A browser that signs its own requests needs a key inside the browser. Don’t put your agent’s key there: make a key for that one browser session and bind the token to it.

browser-agent.ts
import { BageAgent } from "@trybage/sdk/agent";
const sessionKey = BageAgent.generateKey(); // lives only as long as this browser
const session = await agent.signIn({
site: "https://shop.example",
loginHint: "alex@example.com",
scopes: ["browse", "cart"],
bindTo: { privateJwk: sessionKey.privateJwk }, // the SDK proves to Bage you hold it
});
// session.token is bound to sessionKey: your agent's own key can't use it, and neither can anyone without the session key.

Bage lists the session key in your agent’s directory while tokens bound to it are live, and drops it after.

The extension adds the token and a fresh signature to every request the browser makes to the site, including page loads, scripts and XHR, and never to other sites. It runs in any Chromium 137 or later, and in Kernel and other cloud browsers that load extensions.

attach-extension.ts
// Launch Chromium with the extension (extension/src in the bage-auth repo), then hand it the session key and token.
let worker;
for (const candidate of context.serviceWorkers()) if (await candidate.evaluate(() => typeof self.bageConfigure === "function")) worker = candidate;
await worker.evaluate((config) => self.bageConfigure(config), {
signatureAgent: agent.keyDirectory,
privateJwk: sessionKey.privateJwk,
sites: [{ origin: "https://shop.example", token: session.token }],
});
await page.goto("https://shop.example/account"); // the site sees "Scout for Alex"

Call bageConfigure again with the new token after session.refresh(), and with null to stop. Header rules in an extension can’t change per request, so its signature covers the site, the Signature-Agent and the token (not the path), lasts two minutes, and is remade every 30 seconds.

To load it on a cloud browser from the start, zip it with its config: npm run pack -w extension -- bage.zip --config config.json, then upload the zip to the platform.