# Maskbreak guard for one Cloudflare Workers route

This dependency-free module Worker guards **POST /api/checkout** in front of an
existing origin. It sends the browser's `monocle` and `sentinel_fp` values to
`https://maskbreak.com/v1/evaluate` as `token` and `fingerprintEventId`, using
the secret `env.MASKBREAK_API_KEY`. A live, complete `allow` reaches the origin.
An owner can also explicitly enable Turnstile for a complete `review`; the
server must verify a fresh CAPTCHA token before forwarding it. A `block` never
becomes allowed through CAPTCHA. There is no fallback that forwards a failed check.

Download [worker.mjs](./worker.mjs), [panel.mjs](./panel.mjs),
[browser.mjs](./browser.mjs) and [wrangler.jsonc](./wrangler.jsonc).
Keep `worker.mjs` and `panel.mjs` together for the Worker. Host `browser.mjs`
and `panel.mjs` together on your own site for the optional browser companion.
These files are the complete example; no private repository access is required.

| Result | Worker behavior |
| --- | --- |
| Live `allow`, network and device fields present and correctly typed | Forward the original POST to its existing origin |
| Live `block` | 403, no origin request; branded screen or browser dialog |
| Live `review`, default `REVIEW_MODE=hold` | 409, no origin request; your server must implement extra verification |
| Complete live `review`, `REVIEW_MODE=turnstile` | 409 with a challenge; only a fresh server-verified token releases the retried action |
| Missing, empty, duplicate form tokens or `test_*`/sample/sandbox tokens | 403, no evaluation or origin request |
| Missing/non-live key, flagged test/sample/sandbox/degraded result, incomplete allow, invalid response, API error, timeout | 503, no origin request |
| Malformed JSON/UTF-8, oversized body, unsupported media/encoding | 400 / 413 / 415, no origin request |
| Different host/path or non-POST method | 404 / 405, no forwarding |

The API currently returns `status: "success"`, `decision`, `risk_score`,
`network` and, when device lookup succeeds, `device`. It does **not** return a
`live` or `complete` field. The guard validates the existing fields and rejects
`test`, `sample`, `sandbox` and `degraded` flags, including nested degradation.
A network-only allow is unavailable here. A valid live block/review still
refuses the action even when the device fields are absent. Customer rules and
exceptions can change `decision`; the guard honors that final decision.

## Connect the browser

Add the public SDK to the page containing your existing form, and mark the form
`monocle-enriched` (keep your own fields, authentication and CSRF protection):

```html
<script src="https://maskbreak.com/assets/sentinel.js" defer></script>
<form class="monocle-enriched" method="post" action="/api/checkout">
  <!-- Your checkout fields and server-issued CSRF token go here. -->
  <button type="submit">Continue</button>
</form>
```

The SDK injects `monocle` and `sentinel_fp` hidden inputs asynchronously. Wait
until **both** values are nonempty before enabling submission. Handle a blocked
or slow SDK explicitly in your UI; it must not silently submit without tokens.
`Sentinel.collect()` waits for the device layer, but its network token may
still be null: check both returned values. For a JSON submission, map
`collected.token` to `monocle` and `collected.fingerprintEventId` to
`sentinel_fp` alongside your original application fields. Do not put the live
key in browser code. Test the customer's CSP and SDK collection in a real browser.

The Worker accepts UTF-8 `application/json` objects and
`application/x-www-form-urlencoded` forms, up to **64 KiB total**. It counts
actual stream bytes even without Content-Length. Token limits are 32,768
characters for `monocle` and 256 for `sentinel_fp`. No multipart uploads,
compressed request bodies or cross-origin CORS flow are included. Only the two
verification values go to Maskbreak; application fields stay with your origin.

## Configure your own Worker

The dashboard's **Rules → Visitor experience** builder previews the screens and
downloads a `wrangler.jsonc` with your hostname, path, messages and review mode.
Downloading does not deploy anything, enable protection, or save settings to your
existing integration. Review the generated route before deploying; do not replace
an existing Worker without preserving its behavior. Screen text can be written in
your customers' language. Your server can also supply `SCREEN_REVIEW_TITLE`.

If using the generated configuration, `vars.PROTECTED_HOST` and
`vars.PROTECTED_PATH` override the source defaults below.

1. Save the downloaded directory in your application and edit `PROTECTED_HOST` and
   `PROTECTED_PATH` at the top of `worker.mjs`. Use the canonical HTTPS host and
   exact sensitive POST path. The same-path non-POST methods return 405, so
   serve your form page on a different path if it needs GET.
2. Keep the existing origin behind a **proxied DNS record**. Attach this Worker
   as a **Route**, not a Custom Domain or workers.dev endpoint. Same-URL
   `fetch(original)` then goes to the existing origin. A Worker-origin app
   requires a service-binding design and is outside this example.
3. In your copy of `wrangler.jsonc`, replace the empty `routes` array with your
   own route, for example:

   ```json
   "routes": [
     { "pattern": "shop.example.com/api/checkout*", "zone_name": "example.com" }
   ]
   ```

   The final `*` is necessary to cover query strings. The exact path check
   rejects neighboring paths captured by that wildcard. Omitting a scheme in
   the route also captures HTTP, which the Worker refuses. Keep other endpoints
   outside this route; do not attach a site-wide `/*` proxy or replace an existing
   Worker route without integrating its behavior. Ensure the origin accepts no
   unguarded aliases (trailing slashes, alternate casing, encoded paths, hosts or
   method overrides) for this action.
4. In the copied directory, use your Cloudflare account and Wrangler to create
   the new Worker and set its live key interactively. `wrangler secret put
   MASKBREAK_API_KEY` stores the secret; never put its value in this config or
   a command argument. Deploy your configured copy with `wrangler deploy`.
   Neither command is run by this example or its tests. The checked-in config
   publishes no route, workers.dev endpoint or preview URL. Workers do not
   provide a fixed outgoing IP for an existing Maskbreak key IP allowlist;
   confirm that policy is compatible with your integration.

See Cloudflare's current [Routes documentation](https://developers.cloudflare.com/workers/configuration/routing/routes/)
and [secret configuration](https://developers.cloudflare.com/workers/configuration/secrets/).
The module uses [Fetch and its cache options](https://developers.cloudflare.com/workers/runtime-apis/fetch/)
and [manual/error redirect handling](https://developers.cloudflare.com/workers/runtime-apis/request/).
No Node compatibility flag, npm runtime package, or binding besides the secret
is required for hold mode. Turnstile mode additionally requires its public site
key and secret. The modules can also be added in Cloudflare's module editor;
apply the same route, secret and observability settings there.

## Show the review and block screens in your app

Use the companion in your **existing** submission handler, replacing only its
`fetch` call. Keep validation, error handling, success handling and double-submit
prevention. Example for a JSON request after both SDK values have been collected:

```js
import { guardedFetch } from '/maskbreak/browser.mjs';

// Inside your existing form handler. Keep your own CSRF token and app fields.
const response = await guardedFetch('/api/checkout', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrfToken },
  body: JSON.stringify({ ...checkoutFields, monocle: collected.token,
    sentinel_fp: collected.fingerprintEventId }),
});
if (!response.ok) {
  // Stop here. Closing a screen is not approval. Keep your existing error UI.
  return;
}
// Continue your existing success handling with the origin's response.
```

The companion supports same-origin HTTPS POSTs with a replayable JSON string or
URL-encoded body, not uploads or streaming requests. It shows a native, accessible
dialog for the Worker's review/block response. It never replaces your origin's
success response. It retries **once**, only after a review challenge succeeds in
the browser; the Worker re-evaluates the visit and validates the token before any
origin request. It does not retry timeouts, network failures or origin errors.
Disable your submit button while it runs. Keep application idempotency: an origin
timeout may happen after the original action committed.

Native form navigation gets a full-page branded 403/409 with no submitted data
reflected. Automatic challenge-and-retry requires the browser companion; a native
409 does not itself implement a CAPTCHA flow.

### Turnstile: optional, customer-owned, verified on the server

1. Create a Turnstile widget in **your** Cloudflare account restricted to your
   actual frontend hostname. No shared Maskbreak widget is used. For production,
   do not allow localhost or development hosts.
2. Set `REVIEW_MODE` to `turnstile` and `TURNSTILE_SITE_KEY` to the public site key
   in the generated configuration. Store `TURNSTILE_SECRET` with your Worker's
   secret manager (`wrangler secret put TURNSTILE_SECRET`); never put the secret
   in this config, browser code, logs or the dashboard builder.
3. Host the companion and allow `https://challenges.cloudflare.com` in your site's
   `script-src`, `frame-src` and `connect-src`. The dialog uses a shadow-root style
   element; allow its static CSS through your style policy. Do not broadly relax
   an existing strict CSP: add the hash of `panelCSS` or adapt your CSP nonce setup.
4. Test a real review with a fresh token on your staging hostname, then test replay,
   rejection and block. This repository's mocked tests are not a live configuration
   check for your domain.

The Worker's [Siteverify check](https://developers.cloudflare.com/turnstile/get-started/server-side-validation/)
requires literal `success: true`, your exact protected hostname and action
`maskbreak_review`. Tokens are single-use; wrong/expired/replayed tokens stop the
action. Each attempt re-evaluates the visit, and a block or incomplete evaluation
cannot be cleared by CAPTCHA. Only the CAPTCHA token and edge client IP go to
Cloudflare, never the application form. Update your own privacy disclosures for
that processing and redact the `X-Maskbreak-Challenge` header from any edge logs.

### Email verification, MFA or manual review

Keep `REVIEW_MODE=hold`. The kit stops the action and explains that verification
is needed. **It does not implement email, MFA or approval storage for your app.**
Add that flow on your backend, bind approval to the authenticated session and
specific transaction, enforce expiry and replay protection, and only then execute
the action. Do not accept a client `verified=true`, a redirect or a closed dialog
as approval. CAPTCHA is not a substitute for account ownership checks on sensitive
actions such as password recovery or changing payout details.

### Combine VPN and fake-browser evidence

In **Rules → Combined signals**, choose **VPN + fake browser → Block**, then save.
This adds `vpn_antidetect: "block"` to your existing rules. It matches only when
both signals are present on the same evaluation. VPN alone keeps the engine's
review decision unless you explicitly override it. The engine already blocks
antidetect by default; this combined override is useful when you have relaxed
individual signals. The strongest matching configured action wins. IP/visitor
exceptions still outrank signal rules. Preview on past traffic before saving.

## Limits and operational responsibilities

- Input reading and the entire evaluation (including its bounded JSON response)
  each have a 3-second deadline. An allow permits one origin request, with a
  separate 10-second deadline until headers. The origin response streams;
  failures after response headers cannot be converted to a JSON 503. No retries
  are made. An origin timeout can happen **after the action committed**: your
  checkout must use idempotency and reconciliation before offering a retry.
- Forwarding preserves method, URL/query, body bytes, cookies, application
  authorization and CSRF headers. Origin status, response body, redirects and
  cookies pass back to the browser with `no-store` headers. The Maskbreak key is
  added only to the fixed evaluation URL, whose redirects are refused. Origin
  redirects are returned, never followed by the Worker.
- The Worker does not cache or persist evaluations. Both fetches use `cache: "no-store"`;
  all Worker responses set browser and CDN `no-store` headers. Do not override
  these with zone caching rules. No custom logs are emitted. The configuration
  enables logs and sampled traces for operational diagnosis, while automatic
  invocation logs are disabled. Traces can capture request URLs; review sampling,
  access and retention before deployment. See Cloudflare's
  [invocation logging](https://developers.cloudflare.com/workers/observability/logs/workers-logs/)
  and [trace attributes](https://developers.cloudflare.com/workers/observability/traces/spans-and-attributes/).
  Your origin still receives its original body: redact tokens/passwords there
  and in any independent access logs, and keep credentials out of URLs.
- Authentication, authorization, CSRF, rate limiting, application replay protection
  and binding a check to the authenticated action remain your responsibility.
  Hold mode also requires your own server-side approval flow. CAPTCHA only proves
  a challenge was solved; it does not prove account ownership. This stateless
  example does not prove SDK token freshness or bind
  the two browser values to each other or to a transaction. Restrict direct origin access and alternate hostnames, and
  configure Cloudflare route failures to fail closed; platform failures cannot
  be caught by JavaScript. An API/SDK outage deliberately stops this action.

## Maintainer tests and dashboard reuse

Repository maintainers can run `node --test tests/cloudflare-worker.test.js`
from the repository root.
Tests use Node's built-in Fetch objects and mocked fetch/timers, with no network,
database, credentials or new dependencies. They cover the evaluation contract,
original-body forwarding, failure paths, size bounds, deadlines and redirects.
They do not prove Cloudflare DNS/route behavior, runtime compatibility, origin
access restrictions or real SDK/token collection; verify those in your own
staging zone before attaching the route to a sensitive action.

The files in `examples/cloudflare-workers/` are canonical. The site's build can
publish generated copies at `/examples/cloudflare-workers/`, with drift tests;
the dashboard's Workers tab can load `worker.mjs` from there on demand. There
is deliberately no separate hand-maintained snippet asset. This example does
not modify Maskbreak's existing production Cloudflare Workers or routes.
