Saltearse al contenido

Cloudflare Worker

Esta página aún no está disponible en tu idioma.

The Prosopo Protect Cloudflare Worker sits in front of your origin and runs Protect’s decision on every request. On allow it forwards to your origin. On block or challenge it returns immediately with a branded interstitial (or JSON error for API calls) without touching your origin.

Two-part install: read this first

Section titled Two-part install: read this first

A production Prosopo Protect integration is two independent pieces working together. Deploying only one leaves your site unprotected:

  • Part 1: the Cloudflare Worker (server-side). Sits in front of your origin, runs the verdict on every request, and blocks bad traffic before it reaches your server. This is worker.js plus wrangler.toml.
  • Part 2: the Protect JS bundle (client-side). Loaded on every HTML page users visit. Creates the session cookie, collects the bot-signal telemetry the risk engine scores, and shows the captcha overlay when the worker returns a challenge. This is a single <script> tag in your HTML.

Without the worker, no request is scored. Without the bundle, no user ever gets a session cookie, so every /api/* and /forms/* request returns 401 no-session forever. The verify commands under Part 1 test the worker path only, so a green worker with no bundle deployed looks like it’s working while every real user still fails on their first XHR.

Deploy the two parts in either order, but ship both before rollout. The sections below are organised under the same two headings.

Part 1 (server-side) is a tarball you download and deploy:

https://packages.prosopo.io/cloud/Prosopo-Cloudflare-latest.tgz

Extract it and you get:

  • worker.js: the worker code. Self-contained, ~60 KB. Same file for every customer; per-site config lives in wrangler.toml.
  • wrangler.toml: Cloudflare deployment config template.
  • package.json: pinned wrangler version so npm install inside the extracted directory gives you a working toolchain.
  • README.md: quickstart mirroring this guide.

Versioned tarballs are at https://packages.prosopo.io/cloud/Prosopo-Cloudflare-<version>.tgz if you want to pin to a specific release.

Part 2 (client-side) is hosted by us. Either the worker injects the <script> tag automatically (see Deployment modes) or you add one <script> tag pointing at https://js.protect.prosopo.io/protect.bundle.js. Nothing to download or bundle yourself. We publish new versions to that URL; your pages pick them up on their next reload without a redeploy from you.

The client-side bundle can reach your users two ways. Pick one:

Auto-inject (recommended). Set SITE_KEY in wrangler.toml alongside PROTECT_URL. On every allowed HTML response the worker streams the response through Cloudflare’s HTMLRewriter and appends the Protect bundle <script> tags into <head>. You never touch your HTML. Adding, removing, or updating pages requires no coordination — every page the worker serves gets the tags. This is the default DataDome-style install.

Manual snippet. Leave SITE_KEY out of wrangler.toml and paste the two-tag block into a shared header/footer template yourself (see Part 2: Manual snippet). Slightly more work upfront but useful when you want per-template control (e.g. omit the bundle on a specific landing page, or add extra attributes to the script tag).

CLIENT_JWT and PROTECT_URL are required in both modes — the worker calls the Protect API on every request regardless of injection mode.

  • A Cloudflare account with an API token carrying:
    • Account → Workers Scripts → Edit to upload the worker code.
    • Zone → Workers Routes → Edit on the zone you’ll bind the worker to. Only needed when you move from workers.dev to a production route.
  • wrangler installed locally (npm i -g wrangler).
  • A Cloudflare zone for your production domain. A workers.dev subdomain is fine for the initial test.
  • Your Prosopo account manager has:
    • Registered your site key on the Protect dashboard with your intended defaultPathType, endpoint rules, IP-category rules, and interstitial branding. See Portal settings checklist below.
    • Issued you a CLIENT_JWT, a long-lived token authenticating the worker to the Protect API.

Part 1: the Cloudflare Worker (server-side)

Section titled Part 1: the Cloudflare Worker (server-side)

1a. DNS: protect.<your-domain> as DNS-only (grey cloud)

Section titled 1a. DNS: protect.&lt;your-domain&gt; as DNS-only (grey cloud)

The worker talks to Protect at a hostname you own, typically protect.<your-domain>. Create a CNAME record on that hostname pointing at Prosopo:

protect.<your-domain> CNAME protect.prosopo.io

Set the proxy status to DNS only (grey cloud), not proxied (orange cloud). This is not optional.

Why: Prosopo Protect’s risk engine derives bot-signal value from the raw TLS handshake (cipher-suite order, extensions, timing between packets, and fingerprints). If Cloudflare’s proxy terminates the browser’s TLS at its edge, what reaches Prosopo is a fresh TLS connection from Cloudflare’s own stack, identical for every user. The JA4 signals collapse to a single value and the risk-scoring pipeline degrades to noise.

Grey-cloud on protect.<your-domain> only. Every other DNS record on your zone (including your main www record fronted by this worker) stays exactly as it is. This one subdomain needs to point directly at our origin so the browser’s TLS handshake reaches us intact.

1b. Coordinate cert issuance with your account manager

Section titled 1b. Coordinate cert issuance with your account manager

Prosopo issues the TLS certificate for your protect.<your-domain> subdomain via Let’s Encrypt on our infrastructure. This is a two-step handoff:

  1. You: create the CNAME record as DNS-only. Verify it’s live:

    Terminal window
    dig protect.<your-domain>
    # ANSWER SECTION should show a CNAME to protect.prosopo.io
    # and an A record resolving directly to our edge (not Cloudflare's).

    If the A record starts with 104. or 172. you’re still orange-cloud. Flip the record to grey-cloud before proceeding.

  2. Us: email your account manager once DNS is grey-clouded and propagated. We issue the certificate on our end (~30 seconds), then confirm back. Attempting to deploy the worker before we’ve issued the cert results in HTTP 526 (SSL Handshake Failed) on every access-check call, and the worker fails open. No request is actually scored.

Once the cert is in place it auto-renews every 60 days.

Download the tarball:

Terminal window
curl -O https://packages.prosopo.io/cloud/Prosopo-Cloudflare-latest.tgz
tar -xzf Prosopo-Cloudflare-latest.tgz
cd $(tar -tzf Prosopo-Cloudflare-latest.tgz | head -1) # or extract into a fresh dir
npm install

Edit wrangler.toml:

name = "prosopo-protect"
main = "worker.js"
compatibility_date = "2025-01-15"
compatibility_flags = ["nodejs_compat"]
[vars]
PROTECT_URL = "https://protect.<your-domain>"
# Auto-inject mode: set SITE_KEY here and the worker adds the client
# bundle <script> tags to every HTML response. Omit to run in manual
# mode (add the snippet to your HTML yourself — see Part 2).
# SITE_KEY = "<your-site-key>"
# ORIGIN_URL is where the worker forwards allowed requests. Set this
# for any deployment that fronts a real backend. Two hostnames are
# required: the worker sits on <www>, forwards to a different name
# (e.g. origin.<your-domain>, grey-clouded, pointing at your real
# server). Pointing back at <www> would loop through this worker
# forever.
ORIGIN_URL = "https://origin.<your-domain>"
# Static assets are optional. Uncomment when the worker itself serves
# files instead of forwarding to an origin (rare in production; the
# origin usually serves them). `run_worker_first` uses positive
# patterns for paths the worker runs on and `!`-prefixed patterns
# for paths served directly (Protect entirely bypassed).
# [assets]
# directory = "./public"
# binding = "ASSETS"
# run_worker_first = [
# "/*",
# "!/js/*",
# "!/css/*",
# "!/img/*",
# "!/favicon.ico",
# "!/manifest/*",
# "!/apple-touch-icon-*.png",
# "!/robots.txt",
# "!/sitemap.xml",
# ]
[observability]
enabled = true

Push CLIENT_JWT as a secret (persists across deploys) and deploy:

Terminal window
export CLOUDFLARE_API_TOKEN=<your-token>
# One-off: authenticate the worker to the Protect API.
echo "<your-CLIENT_JWT>" | wrangler secret put CLIENT_JWT
wrangler deploy

The worker is now live at https://<name>.<your-subdomain>.workers.dev. Once the verify commands below pass, attach it to your production hostname by uncommenting the routes block in wrangler.toml:

routes = [
{ pattern = "www.<your-domain>/*", zone_name = "<your-domain>" }
]

Then wrangler deploy once more. The worker now runs on every request to www.<your-domain>.

Your account manager notifies you when a new worker.js is available. Replace the file and re-run wrangler deploy. Secrets persist.

Terminal window
WORKER=https://<name>.<your-subdomain>.workers.dev
# 1. HTML: Protect should return "allow" (bootstrap-passthrough) and
# the worker forwards to your origin. Expected: 200, with
# x-prosopo-request-id in the response headers.
curl -sD - -H "Accept: text/html" -H "User-Agent: Mozilla/5.0" "$WORKER/" | head
# 2. JSON, no cookie: Protect returns 401. Expected: 401,
# x-prosopo-status: no-session, x-prosopo-request-id.
curl -sD - -H "Accept: application/json" "$WORKER/api/anything" | head
# 3. POST /forms/* with Accept: text/html and no cookie: same 401.
# This closes the "spoof Accept: text/html on a POST to bypass
# session-required" bot pattern.
curl -sD - -H "Accept: text/html" -X POST "$WORKER/forms/anything" | head

If command 1 returns anything other than 200, you may be hitting your own origin’s bot protection, not Prosopo’s. Cloudflare Bot Management, AWS WAF, and similar tools reliably block curl regardless of what headers you set, because they fingerprint TLS handshakes rather than just User-Agents. Distinguish:

  • If the response has x-prosopo-* headers, the 4xx is from Prosopo. Check x-prosopo-status and x-prosopo-decision. See Troubleshooting.
  • If the response has server: cloudflare or server: awselb and no x-prosopo-* headers, the 4xx is from your origin’s own bot protection blocking curl. Verify command 1 with a real browser instead, or temporarily point ORIGIN_URL at https://httpbin.org for the workers.dev smoke test.

X-Prosopo-Request-Id is echoed on every response Prosopo returns and written to Protect’s verdict audit log. Quote it in support tickets and we can grep straight to the exact verdict that fired. The same ID also appears as Reference: <id> on the blocked interstitial page so users copying that line give you a direct lookup.

Worker verified? Move to Part 2.

Part 2: the Protect JS bundle (client-side)

Section titled Part 2: the Protect JS bundle (client-side)

This part is mandatory. Without the bundle no session cookie is ever set, and the worker returns 401 no-session on every /api/* or /forms/* call in perpetuity. How you get the bundle onto your pages depends on which deployment mode you picked.

2a. Auto-inject (if you set SITE_KEY)

Section titled 2a. Auto-inject (if you set SITE_KEY)

Nothing to do. The worker rewrites every allowed HTML response through Cloudflare’s HTMLRewriter and appends these two tags at the end of <head>:

<script>window.prosopo_config={site_key:"<your-site-key>",protect_url:"https://protect.<your-domain>"};</script>
<script src="https://js.protect.prosopo.io/protect.bundle.js" async></script>

The rewriter is streaming so there’s no measurable latency cost. Non-HTML responses (JSON, CSS, images, XHR replies) pass through untouched.

CSP note. If your site sets a Content-Security-Policy header, add https://js.protect.prosopo.io to script-src so the injected <script> is allowed to load. https://protect.<your-domain> needs to be in connect-src too so the bundle’s fetch and telemetry uploads aren’t blocked.

Skip to Verify the bundle.

2b. Manual snippet (if you haven’t set SITE_KEY)

Section titled 2b. Manual snippet (if you haven’t set SITE_KEY)

Add this snippet to every HTML page your site serves. A shared header or footer template is the usual spot:

<script>
window.prosopo_config = {
site_key: "<your-site-key>",
protect_url: "https://protect.<your-domain>"
};
</script>
<script src="https://js.protect.prosopo.io/protect.bundle.js" async></script>

Both tags are required. The first sets window.prosopo_config before the second loads; the bundle reads that config to know which Protect API to talk to. Skipping the first tag causes the bundle to fall back to the CDN’s own origin (js.protect.prosopo.io) as its API URL, which returns 405 on the CORS preflight for /api/protect/init and hangs the bundle’s patched fetch.

Either mode, once loaded the bundle:

  1. Calls POST https://protect.<your-domain>/api/protect/init.
  2. Receives a prosopo_session cookie scoped to .<your-domain>.
  3. Loads a per-session telemetry runtime script that collects the bot-signal data used for risk scoring.

Every subsequent request from that browser carries the cookie, and the worker looks up a real verdict on /api/* or /forms/* calls.

Section titled 2c. Gate fast form submissions and link clicks (optional)

Users can click Submit before the bundle finishes creating the session. The browser fires a full-page POST with no cookie and gets a 401. The security is correct but the UX is bad.

Opt any form or link into automatic held-until-session-ready behaviour by adding the data-prosopo-gate attribute:

<form action="/forms/search.php" method="post" data-prosopo-gate>
<!-- ... -->
</form>
<nav data-prosopo-gate>
<a href="/section-a/">Section A</a>
<a href="/section-b/">Section B</a>
</nav>

The bundle intercepts marked forms and same-origin links in the capture phase, holds up to 8 seconds waiting for the session, then releases with the cookie in place. The submit button flips disabled and aria-busy while held so the user gets visual feedback. Ctrl/Cmd-click, middle-click, target="_blank", download attributes, hash-only links, and cross-origin links pass through untouched.

Attributes on an ancestor cover every descendant form or link, so <nav data-prosopo-gate> gates every link inside.

Open your site in a real browser with DevTools on the Network tab. On first page load you should see:

  • A POST to https://protect.<your-domain>/api/protect/init returning 200 and a Set-Cookie: prosopo_session=… header.
  • A subsequent GET https://protect.<your-domain>/api/protect/t/<jti> fetching the per-session telemetry runtime.
  • The prosopo_session cookie set with Domain=.<your-domain> for later requests.

If the init call goes to https://js.protect.prosopo.io/api/protect/init and returns 405, the window.prosopo_config block is missing from the snippet. See Troubleshooting.

Before deploying, confirm your Portal instance shows the settings you and your account manager agreed on. Open your Prosopo dashboard, navigate to Sites, your site, and Protect instance, and cross-check:

SettingTypical value for a server-rendered sitePurpose
cnameprotect.<your-domain>Matches the DNS-only CNAME. If empty or pointing at bumblebee1.prosopo.io, the cert isn’t wired up yet; talk to your account manager.
defaultPathTypehtmlServer-rendered pages get the no-cookie bootstrap passthrough so first-time visitors can load the bundle. Set to json only if your site is a pure API.
Endpoint rule: /api/*pathType: jsonAny XHR endpoint requires an established session.
Endpoint rule: /forms/*pathType: jsonForm-action endpoints require a session.
ThresholdsChallenge 0.5, Block 0.85Prosopo defaults. Tune from the dashboard once you’ve seen a few days of real traffic.
EnforcementonPending: allow, onChallenge: captcha, onError: allowFail-open on transient errors, so a Protect outage never blocks legitimate users.
Captcha typefrictionlessInvisible on first attempt. Custom escalation (image challenge, PoW) can be layered on later.
IP category rulesTypically abuser: block, tor: block; add proxy: block when you don’t expect residential-proxy legitimate usersConservative default. Every category rule can be tuned per site.
Interstitial brandingYour logo, colours, copy stringsRendered by the edge on block/challenge, sanitized server-side, no XSS risk.

If any of these don’t match, contact your account manager before deploying. Settings drift here silently produces 401 no-session on paths that should return 200, or blocks IP categories you didn’t intend to.

Deploying to production behind Cloudflare

Section titled Deploying to production behind Cloudflare

Once the workers.dev smoke tests are green, bind the worker to your production hostname by adding a route to wrangler.toml:

routes = [
{ pattern = "www.<your-domain>/*", zone_name = "<your-domain>" }
]

For the worker’s forward to reach your origin you need two hostnames:

  • www.<your-domain>: orange-cloud through Cloudflare, so this worker runs.
  • origin.<your-domain>: grey-cloud DNS-only, pointing at your real origin server. Set ORIGIN_URL = "https://origin.<your-domain>" in wrangler.toml.

Pointing ORIGIN_URL at www.<your-domain> would send the forwarded request back through Cloudflare’s edge, which routes it through this worker again. That’s a loop.

Every request through the worker costs a Protect API call. For hashed CSS, JS, and image assets that never need a bot check, you can bypass Protect entirely using Cloudflare’s run_worker_first list. Uncomment the [assets] block in the wrangler.toml example above, drop your files under ./public, and any path matching a !-prefixed pattern is served by Cloudflare’s asset worker without ever invoking Protect.

Rule of thumb: exclude anything you’d normally cache aggressively on a CDN. Include everything a session should be established on (HTML pages, form endpoints, XHR and JSON endpoints).

Configuring proxy, VPN and datacenter blocking

Section titled Configuring proxy, VPN and datacenter blocking

Blocking policy lives on your Prosopo dashboard, not in the bundle. Two independent rule sources feed the no-cookie edge path:

  • Site settings: recognised IP categories are tor, datacenter, proxy, vpn, abuser, mobile, and crawler. Configure per-category verdicts (allow, challenge, or block).
  • Tiered access rules: support the full rule set of IP CIDR, ASN, IP category, country, User-Agent substring, and JA4 TLS fingerprint. Rules can be scoped to your site or applied globally by Prosopo.

Either source firing blocks the request at the edge. When both match, the more restrictive verdict wins (Block > Challenge > Allow). Policy changes take effect on the next request, no redeploy needed.

The authentication token is stored as a wrangler secret, so rotating it is a one-liner and doesn’t require a redeploy:

Terminal window
echo "<new-CLIENT_JWT>" | wrangler secret put CLIENT_JWT

The next request picks up the new value.

Protect’s API calls from a Cloudflare Worker typically return in under 40 ms (under 2 ms for cached lookups) and are budgeted to a 500 ms hard timeout, with fail-open on transient errors. On an allow verdict the request continues to your origin as normal. On block or challenge the worker returns immediately without an origin fetch, so protected requests are strictly faster than unprotected ones for the blocked path.

Cloudflare Workers doesn’t expose a way to skip TLS verification on outbound fetches. If the Protect API hostname doesn’t have a valid certificate on our side yet, fetch returns HTTP 526 and Protect can’t be reached. This is the failure mode when you deploy before your account manager has issued the cert for protect.<your-domain>. See Coordinate cert issuance.

While debugging a deploy, wrangler tail streams request-by-request logs from the deployed worker:

Terminal window
wrangler tail --format=pretty

Common failure modes with their causes are on the dedicated Troubleshooting page.