From d7223617f4e626b4dddf7360788c20a7f67f5ec4 Mon Sep 17 00:00:00 2001 From: Chris Taylor Date: Tue, 2 Jun 2026 16:38:51 +0100 Subject: [PATCH 01/10] docs: expand spam-email-domain-list section Replaces the one-paragraph mention of the disposable-domain blocklist with full coverage of the five-stage check chain (direct lookup, SSRF-safe validation, HTTPS redirect probe, CNAME chase, MX fallback), provider-side config (`spamEmailDomainsUrls`, scheduler cron) and the standalone `/v1/prosopo/provider/client/spam/email` endpoint with request/response examples. Also updates the evaluation-order list to reflect that the domain-list stage runs last (after the synchronous pattern rules) since it is the most expensive. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/content/docs/en/advanced/spam-filter.mdx | 71 ++++++++++++++++++-- 1 file changed, 65 insertions(+), 6 deletions(-) diff --git a/src/content/docs/en/advanced/spam-filter.mdx b/src/content/docs/en/advanced/spam-filter.mdx index 25f58f997467f..8782e597edbb4 100644 --- a/src/content/docs/en/advanced/spam-filter.mdx +++ b/src/content/docs/en/advanced/spam-filter.mdx @@ -88,16 +88,75 @@ Patterns are validated when saved: ### Spam Email Domain List -A separate toggle (`spamEmailDomainCheckEnabled`) checks the email domain against a maintained list of known disposable/spam email providers. This runs independently of the rules above. +A separate toggle (`spamEmailDomainCheckEnabled`) checks the email domain against a maintained list of known disposable and spam email providers. This runs independently of the pattern rules above and is the most effective single layer against the long tail of throwaway-email services (`tempmail.*`, `guerrillamail.*`, `mailinator.*`, `10minutemail.*`, and the thousands of recycled variants of these). + +When the toggle is enabled, the verification endpoint performs the following sequence for the email's domain: + +1. **Direct lookup.** The domain is lowercased and looked up in the provider's spam-email-domain collection. If it appears on the list, the request is rejected with status `API.SPAM_EMAIL_DOMAIN`. +2. **SSRF safety check.** Before any network call, the candidate domain is validated against an allow-list of public DNS namespaces. Loopback, RFC1918 private ranges, link-local addresses and any domain that resolves into an internal network are rejected outright as spam. This makes the next stage safe to expose to attacker-controlled input. +3. **HTTPS redirect probe.** A short-timeout HTTPS HEAD request is made to the domain. A TLS error is treated as a hard rejection — legitimate email-hosting domains have valid certificates. If the response is a redirect (e.g. `301`/`302`), the redirect target's host is extracted and itself looked up in the spam list. Throwaway services frequently register fresh-looking domains that simply redirect to a known disposable provider; this step catches them. +4. **CNAME lookup.** If there was no redirect, the provider resolves the domain's CNAME chain. A surprisingly large fraction of "novel" disposable services are CNAME aliases of a small set of well-known underlying providers. The aliased domain is checked against the spam list. +5. **MX lookup.** Finally, if no CNAME was found, the domain's MX records are resolved. The first MX exchange is checked against the spam list — disposable services often delegate mail handling to a shared backend, and that backend is on the list. + +If none of these checks match, the email passes the domain-blocklist stage. The full implementation lives at `packages/provider/src/tasks/spam/checkSpamEmail.ts`. + +#### Provider Configuration + +The spam-email-domain list is refreshed on a schedule controlled by the provider operator. Two pieces of provider configuration are involved: + +```jsonc +{ + "spamEmailDomainsUrls": [ + "https://raw.githubusercontent.com/disposable/disposable-email-domains/master/domains.txt", + "https://example.com/your-own-curated-feed.txt" + ], + "scheduledTasks": { + "spamEmailDomainsScheduler": { + "schedule": "0 */6 * * *" + } + } +} +``` + +- **`spamEmailDomainsUrls`** — an array of HTTP(S) URLs that each return a newline-delimited list of domains. Lines beginning with `#` or `//` are treated as comments and skipped. Each domain is validated and lowercased before insertion. Multiple URLs are merged into a single deduplicated set, so operators can combine community-maintained lists with private feeds. +- **`scheduledTasks.spamEmailDomainsScheduler.schedule`** — a standard cron expression that controls how often the lists are re-fetched. Each URL is cached on disk between runs (default cache directory `./spam-cache`) so that repeated fetches do not hammer upstream providers. + +The scheduled task runs as `ScheduledTaskNames.UpdateSpamEmailDomains` and refuses to start a second concurrent invocation, so it is safe to schedule aggressively. Results — including the number of domains processed — are written to the provider's scheduled-task-status collection for audit. + +#### Standalone Endpoint + +Once the domain check is enabled for a site, dapps can additionally call the standalone endpoint to check an email address without performing a CAPTCHA verification: + +```http +POST /v1/prosopo/provider/client/spam/email +Content-Type: application/json + +{ + "email": "user@example.com", + "dapp": "YOUR_SITE_KEY" +} +``` + +Response: + +```json +{ + "isSpam": true, + "emailDomain": "example.com" +} +``` + +The endpoint enforces the site's `spamEmailDomainCheckEnabled` setting (a request from a site without the feature enabled is rejected with `API.BAD_REQUEST`) and is rate-limited per the provider's configured `rateLimits` for this path. ## Evaluation Order -When the email filter is enabled and an email is provided, rules are evaluated in this order: +When the email filter is enabled and an email is provided, the verification endpoint evaluates the email in this order: -1. **Email validity** — malformed addresses are rejected -2. **Maximum dots** — if configured, checked first -3. **Default patterns** — if enabled, evaluated next -4. **Custom regex blocklist** — each pattern is tested in order; first match wins +1. **Email validity** — malformed addresses are rejected immediately. +2. **Maximum dots** — if configured, checked first. +3. **Default patterns** — if enabled, evaluated next. +4. **Custom regex blocklist** — each pattern is tested in order; first match wins. +5. **Spam-email-domain list** — if `spamEmailDomainCheckEnabled` is on, the direct lookup, redirect probe, CNAME lookup and MX lookup chain runs last because it is the most expensive stage. Evaluation stops at the first match. The rejection reason is recorded for audit purposes. From edafd8b3f572b5d2c8d1ac50a5744fc110198654 Mon Sep 17 00:00:00 2001 From: Chris Taylor Date: Tue, 2 Jun 2026 16:45:05 +0100 Subject: [PATCH 02/10] docs: trim spam-email-domain-list section for marketing tone Cuts implementation detail that doesn't belong on customer-facing docs: the SSRF safety check, source-code paths, the provider-side feed URLs and cron config, and the per-step DNS-chase explanation. What's left: what the toggle does, what gets caught (including the redirect/CNAME/MX chase, named but not over-explained), the rejection status code, and the standalone endpoint contract. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/content/docs/en/advanced/spam-filter.mdx | 43 +++----------------- 1 file changed, 6 insertions(+), 37 deletions(-) diff --git a/src/content/docs/en/advanced/spam-filter.mdx b/src/content/docs/en/advanced/spam-filter.mdx index 8782e597edbb4..ba4515663c5f4 100644 --- a/src/content/docs/en/advanced/spam-filter.mdx +++ b/src/content/docs/en/advanced/spam-filter.mdx @@ -88,44 +88,15 @@ Patterns are validated when saved: ### Spam Email Domain List -A separate toggle (`spamEmailDomainCheckEnabled`) checks the email domain against a maintained list of known disposable and spam email providers. This runs independently of the pattern rules above and is the most effective single layer against the long tail of throwaway-email services (`tempmail.*`, `guerrillamail.*`, `mailinator.*`, `10minutemail.*`, and the thousands of recycled variants of these). +Toggle **Spam email domain checking** (`spamEmailDomainCheckEnabled`) on to block signups from known disposable and throwaway email providers — `tempmail.*`, `guerrillamail.*`, `mailinator.*` and the thousands of recycled variants spammers cycle through. -When the toggle is enabled, the verification endpoint performs the following sequence for the email's domain: +The check goes beyond matching the address's domain directly: Prosopo also follows the domain through any HTTP redirect, CNAME alias and MX record, so newly-registered throwaway domains can't bypass the list by simply pointing at a known disposable backend. -1. **Direct lookup.** The domain is lowercased and looked up in the provider's spam-email-domain collection. If it appears on the list, the request is rejected with status `API.SPAM_EMAIL_DOMAIN`. -2. **SSRF safety check.** Before any network call, the candidate domain is validated against an allow-list of public DNS namespaces. Loopback, RFC1918 private ranges, link-local addresses and any domain that resolves into an internal network are rejected outright as spam. This makes the next stage safe to expose to attacker-controlled input. -3. **HTTPS redirect probe.** A short-timeout HTTPS HEAD request is made to the domain. A TLS error is treated as a hard rejection — legitimate email-hosting domains have valid certificates. If the response is a redirect (e.g. `301`/`302`), the redirect target's host is extracted and itself looked up in the spam list. Throwaway services frequently register fresh-looking domains that simply redirect to a known disposable provider; this step catches them. -4. **CNAME lookup.** If there was no redirect, the provider resolves the domain's CNAME chain. A surprisingly large fraction of "novel" disposable services are CNAME aliases of a small set of well-known underlying providers. The aliased domain is checked against the spam list. -5. **MX lookup.** Finally, if no CNAME was found, the domain's MX records are resolved. The first MX exchange is checked against the spam list — disposable services often delegate mail handling to a shared backend, and that backend is on the list. - -If none of these checks match, the email passes the domain-blocklist stage. The full implementation lives at `packages/provider/src/tasks/spam/checkSpamEmail.ts`. - -#### Provider Configuration - -The spam-email-domain list is refreshed on a schedule controlled by the provider operator. Two pieces of provider configuration are involved: - -```jsonc -{ - "spamEmailDomainsUrls": [ - "https://raw.githubusercontent.com/disposable/disposable-email-domains/master/domains.txt", - "https://example.com/your-own-curated-feed.txt" - ], - "scheduledTasks": { - "spamEmailDomainsScheduler": { - "schedule": "0 */6 * * *" - } - } -} -``` - -- **`spamEmailDomainsUrls`** — an array of HTTP(S) URLs that each return a newline-delimited list of domains. Lines beginning with `#` or `//` are treated as comments and skipped. Each domain is validated and lowercased before insertion. Multiple URLs are merged into a single deduplicated set, so operators can combine community-maintained lists with private feeds. -- **`scheduledTasks.spamEmailDomainsScheduler.schedule`** — a standard cron expression that controls how often the lists are re-fetched. Each URL is cached on disk between runs (default cache directory `./spam-cache`) so that repeated fetches do not hammer upstream providers. - -The scheduled task runs as `ScheduledTaskNames.UpdateSpamEmailDomains` and refuses to start a second concurrent invocation, so it is safe to schedule aggressively. Results — including the number of domains processed — are written to the provider's scheduled-task-status collection for audit. +The blocklist is maintained and refreshed for you — there's nothing to configure beyond the toggle. Rejected requests come back with status `API.SPAM_EMAIL_DOMAIN`. #### Standalone Endpoint -Once the domain check is enabled for a site, dapps can additionally call the standalone endpoint to check an email address without performing a CAPTCHA verification: +You can also check an address without running a full CAPTCHA verification: ```http POST /v1/prosopo/provider/client/spam/email @@ -137,8 +108,6 @@ Content-Type: application/json } ``` -Response: - ```json { "isSpam": true, @@ -146,7 +115,7 @@ Response: } ``` -The endpoint enforces the site's `spamEmailDomainCheckEnabled` setting (a request from a site without the feature enabled is rejected with `API.BAD_REQUEST`) and is rate-limited per the provider's configured `rateLimits` for this path. +Available to sites with the domain check enabled, and rate-limited per site key. ## Evaluation Order @@ -156,7 +125,7 @@ When the email filter is enabled and an email is provided, the verification endp 2. **Maximum dots** — if configured, checked first. 3. **Default patterns** — if enabled, evaluated next. 4. **Custom regex blocklist** — each pattern is tested in order; first match wins. -5. **Spam-email-domain list** — if `spamEmailDomainCheckEnabled` is on, the direct lookup, redirect probe, CNAME lookup and MX lookup chain runs last because it is the most expensive stage. +5. **Spam-email-domain list** — if enabled, the domain (and any redirect, CNAME or MX it points at) is checked against the maintained blocklist. Evaluation stops at the first match. The rejection reason is recorded for audit purposes. From 501e7b9debec1bbb7d1e2e50e99229c496b284a6 Mon Sep 17 00:00:00 2001 From: Chris Taylor Date: Wed, 10 Jun 2026 13:32:41 +0100 Subject: [PATCH 03/10] fix(docs): correct cross-language inconsistencies and add missing features Cross-language fixes (en + de/es/fr/it/pt-br): - Fix broken React integration links in welcome and client-side-rendering (pointed at /angular-integration/ instead of /react-integration/) - Fix invisible-captcha server-side example using reCAPTCHA-style fields (response/remoteip/result.success) instead of Prosopo's (token/ip/verified) - Fix server-side-verification SDK example: ProsopoServer requires a pair argument; isVerified returns a VerificationResponse object, not a boolean - Fix tier naming "Pro and Enterprise" -> "Professional and Enterprise" to match the Tier enum in the portal English-only: - Add ASN as a documented access-control rule field (already supported by the rule editor and policy schema, just undocumented) - Reword traffic-filter "Providing the IP Address" to lead with what actually happens (filters always run against the session-initiation IP; passing ip overrides with a fresh lookup) instead of implying the IP field is required Co-Authored-By: Claude Opus 4.7 (1M context) --- .../docs/de/basics/client-side-rendering.mdx | 2 +- .../docs/de/basics/invisible-captcha.mdx | 10 +++++----- .../de/basics/server-side-verification.mdx | 13 ++++++++---- src/content/docs/de/welcome/index.mdx | 2 +- .../docs/en/advanced/access-control-rules.mdx | 20 +++++++++++++++++++ .../docs/en/advanced/traffic-filter.mdx | 6 +++--- .../docs/en/basics/client-side-rendering.mdx | 2 +- .../docs/en/basics/invisible-captcha.mdx | 10 +++++----- .../en/basics/server-side-verification.mdx | 13 ++++++++---- src/content/docs/en/welcome/index.mdx | 2 +- .../docs/es/basics/client-side-rendering.mdx | 2 +- .../docs/es/basics/invisible-captcha.mdx | 10 +++++----- .../es/basics/server-side-verification.mdx | 13 ++++++++---- src/content/docs/es/welcome/index.mdx | 2 +- .../docs/fr/basics/client-side-rendering.mdx | 2 +- .../docs/fr/basics/invisible-captcha.mdx | 10 +++++----- .../fr/basics/server-side-verification.mdx | 13 ++++++++---- src/content/docs/fr/welcome/index.mdx | 2 +- .../docs/it/basics/client-side-rendering.mdx | 2 +- .../docs/it/basics/invisible-captcha.mdx | 10 +++++----- .../it/basics/server-side-verification.mdx | 13 ++++++++---- src/content/docs/it/welcome/index.mdx | 2 +- .../pt-br/basics/client-side-rendering.mdx | 2 +- .../docs/pt-br/basics/invisible-captcha.mdx | 10 +++++----- .../pt-br/basics/server-side-verification.mdx | 13 ++++++++---- src/content/docs/pt-br/welcome/index.mdx | 2 +- 26 files changed, 119 insertions(+), 69 deletions(-) diff --git a/src/content/docs/de/basics/client-side-rendering.mdx b/src/content/docs/de/basics/client-side-rendering.mdx index bbcce947751bf..3708561701a69 100644 --- a/src/content/docs/de/basics/client-side-rendering.mdx +++ b/src/content/docs/de/basics/client-side-rendering.mdx @@ -160,7 +160,7 @@ Sie können beim Rendern der Procaptcha-Komponente jeden der folgenden CAPTCHA-T Verschiedene Frameworks wurden mit Procaptcha integriert. Die Dokumentation für jedes Framework finden Sie unten: -- [React Integration](/de/framework-integrations/angular-integration/) +- [React Integration](/de/framework-integrations/react-integration/) - [Vue Integration](/de/framework-integrations/vue-integration/) - [Angular Integration](/de/framework-integrations/angular-integration/) - [Svelte Integration](/de/framework-integrations/svelte-integration/) diff --git a/src/content/docs/de/basics/invisible-captcha.mdx b/src/content/docs/de/basics/invisible-captcha.mdx index 864c739944d94..e7dcc5595906a 100644 --- a/src/content/docs/de/basics/invisible-captcha.mdx +++ b/src/content/docs/de/basics/invisible-captcha.mdx @@ -11,7 +11,7 @@ Unsichtbares CAPTCHA befindet sich derzeit in der **Beta**-Phase. Funktionen und ::: :::note[Stufen-Beschränkung] -Unsichtbares CAPTCHA ist nur für Benutzer der **Pro- und Enterprise**-Stufen verfügbar. Benutzer der kostenlosen Stufe können nicht auf diese Funktion zugreifen. +Unsichtbares CAPTCHA ist nur für Benutzer der **Professional- und Enterprise**-Stufen verfügbar. Benutzer der kostenlosen Stufe können nicht auf diese Funktion zugreifen. ::: ## Übersicht @@ -219,18 +219,18 @@ const response = await fetch('https://api.prosopo.io/siteverify', { }, body: JSON.stringify({ secret: 'your_secret_key', - response: token, // Token from Procaptcha callback - remoteip: userIP // Optional + token: token, // Token from Procaptcha callback + ip: userIP // Optional }) }); const result = await response.json(); -if (result.success) { +if (result.verified) { // Procaptcha verified successfully console.log('Verification successful'); } else { // Verification failed - console.log('Verification failed:', result['error-codes']); + console.log('Verification failed:', result.status); } ``` diff --git a/src/content/docs/de/basics/server-side-verification.mdx b/src/content/docs/de/basics/server-side-verification.mdx index 28433e7dcc896..bffecdca825f4 100644 --- a/src/content/docs/de/basics/server-side-verification.mdx +++ b/src/content/docs/de/basics/server-side-verification.mdx @@ -95,7 +95,8 @@ async function verifyToken(token) { headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ secret: 'your_secret_key', token }), }); - return response.json().verified || false; // Return verified field, default to false + const data = await response.json(); + return data.verified || false; // Return verified field, default to false } ``` @@ -193,7 +194,8 @@ Um eine Benutzerantwort mit JavaScript / TypeScript zu verifizieren, importieren die `procaptcha-response` POST-Daten. Typen können aus `@prosopo/types` importiert werden. ```typescript -import {ProsopoServer} from '@prosopo/server' +import {ProsopoServer, getServerConfig} from '@prosopo/server' +import {getPair} from '@prosopo/keyring' import {ApiParams} from '@prosopo/types' ... @@ -204,10 +206,13 @@ const payload = JSON.parse(event.body) const procaptchaResponse = payload[ApiParams.procaptchaResponse] // initialise the `ProsopoServer` class -const prosopoServer = new ProsopoServer(config) +const config = getServerConfig() +const pair = getPair(process.env.PROSOPO_SITE_PRIVATE_KEY, config.account.address) +const prosopoServer = new ProsopoServer(config, pair) // check if the captcha response is verified -if (await prosopoServer.isVerified(procaptchaResponse)) { +const result = await prosopoServer.isVerified(procaptchaResponse) +if (result.verified) { // perform CAPTCHA protected action } ``` diff --git a/src/content/docs/de/welcome/index.mdx b/src/content/docs/de/welcome/index.mdx index ea65d9266077d..11ae07c2ee25b 100644 --- a/src/content/docs/de/welcome/index.mdx +++ b/src/content/docs/de/welcome/index.mdx @@ -26,7 +26,7 @@ Sie können ein vollständiges Beispiel zur Implementierung von Procaptcha in ei Verschiedene Frameworks wurden mit Procaptcha integriert. Die Dokumentation für jedes Framework finden Sie unten: -- [React Integration](/de/framework-integrations/angular-integration/) +- [React Integration](/de/framework-integrations/react-integration/) - [Vue Integration](/de/framework-integrations/vue-integration/) - [Angular Integration](/de/framework-integrations/angular-integration/) - [Svelte Integration](/de/framework-integrations/svelte-integration/) diff --git a/src/content/docs/en/advanced/access-control-rules.mdx b/src/content/docs/en/advanced/access-control-rules.mdx index 3d28033c5549f..93f4bdc505098 100644 --- a/src/content/docs/en/advanced/access-control-rules.mdx +++ b/src/content/docs/en/advanced/access-control-rules.mdx @@ -94,6 +94,16 @@ Match requests from specific countries using ISO 3166-1 alpha-2 country codes. **Use case:** Apply stricter or more lenient policies for specific geographic regions. +#### ASN + +Match the Autonomous System Number (ASN) of the IP's network. Useful for blocking or restricting traffic from a specific hosting provider, ISP, or VPN/proxy network without enumerating every IP they own. + +**Format:** Numeric AS number (e.g., `14061`, `32934`) + +**Examples:** `14061` (DigitalOcean), `32934` (Meta), `13335` (Cloudflare) + +**Use case:** Restrict or block traffic from cloud hosting networks frequently used by bots, or apply policies to whole ISPs in response to abuse patterns. + ### Operators Currently, only the **equals** operator is supported. The condition matches when the field value exactly equals the specified value. @@ -296,6 +306,16 @@ JA4 Hash equals "t13d1516h2_8daaf6152771_a278895b5b6a" Policy: Block ``` +### ASN-Based Blocking + +Restrict traffic from a whole hosting provider or ISP by AS number: + +```typescript +// Restrict traffic from a cloud hosting ASN +ASN equals "14061" +Policy: Proof of Work, difficulty 5 +``` + ## Considerations ### Account-Wide Scope diff --git a/src/content/docs/en/advanced/traffic-filter.mdx b/src/content/docs/en/advanced/traffic-filter.mdx index 1b39269736d0e..f100bdda7da95 100644 --- a/src/content/docs/en/advanced/traffic-filter.mdx +++ b/src/content/docs/en/advanced/traffic-filter.mdx @@ -36,7 +36,9 @@ Traffic filter checks run before other verification logic (captcha correctness, ## Providing the IP Address -Traffic filters require the user's IP address. Pass it in the `ip` field when calling the server-side verification endpoint: +Traffic filters always run. By default they evaluate the IP recorded when the user initiated the captcha session (the browser's IP at session-start time, captured by the provider when the widget first contacted it). + +If the end user's IP may have changed between solving the captcha and your server calling `/verify` (e.g. they moved networks), pass the current IP in the optional `ip` field. The provider then re-resolves IP info against that "now" IP and runs the filters on the fresh result: ```json { @@ -48,8 +50,6 @@ Traffic filters require the user's IP address. Pass it in the `ip` field when ca See the [Server-side Verification](/en/basics/server-side-verification/#optional-ip-address) docs for details. -If no IP address is provided, traffic filters are still evaluated but only the abusive-network filter will fire (using the connecting IP from the request itself). - ## Filter Details ### VPN diff --git a/src/content/docs/en/basics/client-side-rendering.mdx b/src/content/docs/en/basics/client-side-rendering.mdx index 97f1aeed19dd3..ebea4a0bea3f5 100644 --- a/src/content/docs/en/basics/client-side-rendering.mdx +++ b/src/content/docs/en/basics/client-side-rendering.mdx @@ -160,7 +160,7 @@ You can choose to implement any of the following types of captcha when rendering Various frameworks have been integrated with Procaptcha. You can find the documentation for each framework below: -- [React Integration](/en/framework-integrations/angular-integration/) +- [React Integration](/en/framework-integrations/react-integration/) - [Vue Integration](/en/framework-integrations/vue-integration/) - [Angular Integration](/en/framework-integrations/angular-integration/) - [Svelte Integration](/en/framework-integrations/svelte-integration/) diff --git a/src/content/docs/en/basics/invisible-captcha.mdx b/src/content/docs/en/basics/invisible-captcha.mdx index c7097e248ce88..2c26f8c33d187 100644 --- a/src/content/docs/en/basics/invisible-captcha.mdx +++ b/src/content/docs/en/basics/invisible-captcha.mdx @@ -11,7 +11,7 @@ Invisible CAPTCHA is currently in **beta**. Features and behavior may change in ::: :::note[Tier Restriction] -Invisible CAPTCHA is only available for **Pro and Enterprise** tier users. Free tier users cannot access this feature. +Invisible CAPTCHA is only available for **Professional and Enterprise** tier users. Free tier users cannot access this feature. ::: ## Overview @@ -219,18 +219,18 @@ const response = await fetch('https://api.prosopo.io/siteverify', { }, body: JSON.stringify({ secret: 'your_secret_key', - response: token, // Token from Procaptcha callback - remoteip: userIP // Optional + token: token, // Token from Procaptcha callback + ip: userIP // Optional }) }); const result = await response.json(); -if (result.success) { +if (result.verified) { // Procaptcha verified successfully console.log('Verification successful'); } else { // Verification failed - console.log('Verification failed:', result['error-codes']); + console.log('Verification failed:', result.status); } ``` diff --git a/src/content/docs/en/basics/server-side-verification.mdx b/src/content/docs/en/basics/server-side-verification.mdx index a1791592b4ef2..85d059c5e02fc 100644 --- a/src/content/docs/en/basics/server-side-verification.mdx +++ b/src/content/docs/en/basics/server-side-verification.mdx @@ -112,7 +112,8 @@ on the request. This is optional, but recommended for better accuracy. To do thi headers: {'Content-Type': 'application/json'}, body: JSON.stringify({secret: 'your_secret_key', token}), }); - return response.json().verified || false; // Return verified field, default to false + const data = await response.json(); + return data.verified || false; // Return verified field, default to false } ``` @@ -210,7 +211,8 @@ To verify a user's response using JavaScript / TypeScript, simpy import the `ver the `procaptcha-response` POST data. Types can be imported from `@prosopo/types`. ```typescript -import {ProsopoServer} from '@prosopo/server' +import {ProsopoServer, getServerConfig} from '@prosopo/server' +import {getPair} from '@prosopo/keyring' import {ApiParams} from '@prosopo/types' ... @@ -221,10 +223,13 @@ const payload = JSON.parse(event.body) const procaptchaResponse = payload[ApiParams.procaptchaResponse] // initialise the `ProsopoServer` class -const prosopoServer = new ProsopoServer(config) +const config = getServerConfig() +const pair = getPair(process.env.PROSOPO_SITE_PRIVATE_KEY, config.account.address) +const prosopoServer = new ProsopoServer(config, pair) // check if the captcha response is verified -if (await prosopoServer.isVerified(procaptchaResponse)) { +const result = await prosopoServer.isVerified(procaptchaResponse) +if (result.verified) { // perform CAPTCHA protected action } ``` diff --git a/src/content/docs/en/welcome/index.mdx b/src/content/docs/en/welcome/index.mdx index 59efd0ee1061b..ebb8fc141ee20 100644 --- a/src/content/docs/en/welcome/index.mdx +++ b/src/content/docs/en/welcome/index.mdx @@ -30,7 +30,7 @@ of how to run the examples are in the documentation at the previous links. Various frameworks have been integrated with Procaptcha. You can find the documentation for each framework below: -- [React Integration](/en/framework-integrations/angular-integration/) +- [React Integration](/en/framework-integrations/react-integration/) - [Vue Integration](/en/framework-integrations/vue-integration/) - [Angular Integration](/en/framework-integrations/angular-integration/) - [Svelte Integration](/en/framework-integrations/svelte-integration/) diff --git a/src/content/docs/es/basics/client-side-rendering.mdx b/src/content/docs/es/basics/client-side-rendering.mdx index 867688d17d8f0..e4e3be8a7228a 100644 --- a/src/content/docs/es/basics/client-side-rendering.mdx +++ b/src/content/docs/es/basics/client-side-rendering.mdx @@ -148,7 +148,7 @@ Puede elegir implementar cualquiera de los siguientes tipos de captcha al render Varios frameworks se han integrado con Procaptcha. Puede encontrar la documentación para cada framework a continuación: -- [Integración con React](/es/framework-integrations/angular-integration/) +- [Integración con React](/es/framework-integrations/react-integration/) - [Integración con Vue](/es/framework-integrations/vue-integration/) - [Integración con Angular](/es/framework-integrations/angular-integration/) - [Integración con Svelte](/es/framework-integrations/svelte-integration/) diff --git a/src/content/docs/es/basics/invisible-captcha.mdx b/src/content/docs/es/basics/invisible-captcha.mdx index 457af622846c9..57cf9202514ed 100644 --- a/src/content/docs/es/basics/invisible-captcha.mdx +++ b/src/content/docs/es/basics/invisible-captcha.mdx @@ -11,7 +11,7 @@ CAPTCHA invisible está actualmente en **beta**. Las características y el compo ::: :::note[Restricción de nivel] -CAPTCHA invisible solo está disponible para usuarios de nivel **Pro y Enterprise**. Los usuarios del nivel gratuito no pueden acceder a esta función. +CAPTCHA invisible solo está disponible para usuarios de nivel **Professional y Enterprise**. Los usuarios del nivel gratuito no pueden acceder a esta función. ::: ## Descripción general @@ -222,18 +222,18 @@ const response = await fetch('https://api.prosopo.io/siteverify', { }, body: JSON.stringify({ secret: 'your_secret_key', - response: token, // Token from Procaptcha callback - remoteip: userIP // Optional + token: token, // Token from Procaptcha callback + ip: userIP // Optional }) }); const result = await response.json(); -if (result.success) { +if (result.verified) { // Procaptcha verified successfully console.log('Verification successful'); } else { // Verification failed - console.log('Verification failed:', result['error-codes']); + console.log('Verification failed:', result.status); } ``` diff --git a/src/content/docs/es/basics/server-side-verification.mdx b/src/content/docs/es/basics/server-side-verification.mdx index 117e1462767ca..d22c4e42793ee 100644 --- a/src/content/docs/es/basics/server-side-verification.mdx +++ b/src/content/docs/es/basics/server-side-verification.mdx @@ -90,7 +90,8 @@ async function verifyToken(token) { headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ secret: 'your_secret_key', token }), }); - return response.json().verified || false; // Return verified field, default to false + const data = await response.json(); + return data.verified || false; // Return verified field, default to false } ``` @@ -187,7 +188,8 @@ npm install @prosopo/server Para verificar la respuesta de un usuario usando JavaScript / TypeScript, simplemente importe la función `verify` desde `@prosopo/server` y pásele los datos POST `procaptcha-response`. Los tipos se pueden importar desde `@prosopo/types`. ```typescript -import {ProsopoServer} from '@prosopo/server' +import {ProsopoServer, getServerConfig} from '@prosopo/server' +import {getPair} from '@prosopo/keyring' import {ApiParams} from '@prosopo/types' ... @@ -198,10 +200,13 @@ const payload = JSON.parse(event.body) const procaptchaResponse = payload[ApiParams.procaptchaResponse] // initialise the `ProsopoServer` class -const prosopoServer = new ProsopoServer(config) +const config = getServerConfig() +const pair = getPair(process.env.PROSOPO_SITE_PRIVATE_KEY, config.account.address) +const prosopoServer = new ProsopoServer(config, pair) // check if the captcha response is verified -if (await prosopoServer.isVerified(procaptchaResponse)) { +const result = await prosopoServer.isVerified(procaptchaResponse) +if (result.verified) { // perform CAPTCHA protected action } ``` diff --git a/src/content/docs/es/welcome/index.mdx b/src/content/docs/es/welcome/index.mdx index bc8b3d532018d..72bcfb87f27e7 100644 --- a/src/content/docs/es/welcome/index.mdx +++ b/src/content/docs/es/welcome/index.mdx @@ -30,7 +30,7 @@ de cómo ejecutar los ejemplos están en la documentación en los enlaces anteri Varios frameworks se han integrado con Procaptcha. Puede encontrar la documentación para cada framework a continuación: -- [Integración con React](/es/framework-integrations/angular-integration/) +- [Integración con React](/es/framework-integrations/react-integration/) - [Integración con Vue](/es/framework-integrations/vue-integration/) - [Integración con Angular](/es/framework-integrations/angular-integration/) - [Integración con Svelte](/es/framework-integrations/svelte-integration/) diff --git a/src/content/docs/fr/basics/client-side-rendering.mdx b/src/content/docs/fr/basics/client-side-rendering.mdx index babf8996e7e2c..3859a6ea015c6 100644 --- a/src/content/docs/fr/basics/client-side-rendering.mdx +++ b/src/content/docs/fr/basics/client-side-rendering.mdx @@ -147,7 +147,7 @@ Vous pouvez choisir d'implémenter l'un des types de captcha suivants lors du re Différents frameworks ont été intégrés avec Procaptcha. Vous pouvez trouver la documentation pour chaque framework ci-dessous : -- [Intégration React](/fr/framework-integrations/angular-integration/) +- [Intégration React](/fr/framework-integrations/react-integration/) - [Intégration Vue](/fr/framework-integrations/vue-integration/) - [Intégration Angular](/fr/framework-integrations/angular-integration/) - [Intégration Svelte](/fr/framework-integrations/svelte-integration/) diff --git a/src/content/docs/fr/basics/invisible-captcha.mdx b/src/content/docs/fr/basics/invisible-captcha.mdx index e7ac87c5f3ffc..c290a4521ebef 100644 --- a/src/content/docs/fr/basics/invisible-captcha.mdx +++ b/src/content/docs/fr/basics/invisible-captcha.mdx @@ -11,7 +11,7 @@ Le CAPTCHA invisible est actuellement en **bêta**. Les fonctionnalités et le c ::: :::note[Restriction de niveau] -Le CAPTCHA invisible n'est disponible que pour les utilisateurs des niveaux **Pro et Enterprise**. Les utilisateurs du niveau gratuit ne peuvent pas accéder à cette fonctionnalité. +Le CAPTCHA invisible n'est disponible que pour les utilisateurs des niveaux **Professional et Enterprise**. Les utilisateurs du niveau gratuit ne peuvent pas accéder à cette fonctionnalité. ::: ## Aperçu @@ -219,18 +219,18 @@ const response = await fetch('https://api.prosopo.io/siteverify', { }, body: JSON.stringify({ secret: 'your_secret_key', - response: token, // Token from Procaptcha callback - remoteip: userIP // Optional + token: token, // Token from Procaptcha callback + ip: userIP // Optional }) }); const result = await response.json(); -if (result.success) { +if (result.verified) { // Procaptcha verified successfully console.log('Verification successful'); } else { // Verification failed - console.log('Verification failed:', result['error-codes']); + console.log('Verification failed:', result.status); } ``` diff --git a/src/content/docs/fr/basics/server-side-verification.mdx b/src/content/docs/fr/basics/server-side-verification.mdx index 71e30657132ad..dafd80a4e07d1 100644 --- a/src/content/docs/fr/basics/server-side-verification.mdx +++ b/src/content/docs/fr/basics/server-side-verification.mdx @@ -90,7 +90,8 @@ async function verifyToken(token) { headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ secret: 'your_secret_key', token }), }); - return response.json().verified || false; // Return verified field, default to false + const data = await response.json(); + return data.verified || false; // Return verified field, default to false } ``` @@ -187,7 +188,8 @@ npm install @prosopo/server Pour vérifier la réponse d'un utilisateur en utilisant JavaScript / TypeScript, importez simplement la fonction `verify` de `@prosopo/server` et passez-lui les données POST `procaptcha-response`. Les types peuvent être importés de `@prosopo/types`. ```typescript -import {ProsopoServer} from '@prosopo/server' +import {ProsopoServer, getServerConfig} from '@prosopo/server' +import {getPair} from '@prosopo/keyring' import {ApiParams} from '@prosopo/types' ... @@ -198,10 +200,13 @@ const payload = JSON.parse(event.body) const procaptchaResponse = payload[ApiParams.procaptchaResponse] // initialise the `ProsopoServer` class -const prosopoServer = new ProsopoServer(config) +const config = getServerConfig() +const pair = getPair(process.env.PROSOPO_SITE_PRIVATE_KEY, config.account.address) +const prosopoServer = new ProsopoServer(config, pair) // check if the captcha response is verified -if (await prosopoServer.isVerified(procaptchaResponse)) { +const result = await prosopoServer.isVerified(procaptchaResponse) +if (result.verified) { // perform CAPTCHA protected action } ``` diff --git a/src/content/docs/fr/welcome/index.mdx b/src/content/docs/fr/welcome/index.mdx index 75a51d164b62d..3874bea8157f4 100644 --- a/src/content/docs/fr/welcome/index.mdx +++ b/src/content/docs/fr/welcome/index.mdx @@ -26,7 +26,7 @@ Vous pouvez voir un exemple de bout en bout de la façon d'implémenter Procaptc Différents frameworks ont été intégrés avec Procaptcha. Vous pouvez trouver la documentation pour chaque framework ci-dessous : -- [Intégration React](/fr/framework-integrations/angular-integration/) +- [Intégration React](/fr/framework-integrations/react-integration/) - [Intégration Vue](/fr/framework-integrations/vue-integration/) - [Intégration Angular](/fr/framework-integrations/angular-integration/) - [Intégration Svelte](/fr/framework-integrations/svelte-integration/) diff --git a/src/content/docs/it/basics/client-side-rendering.mdx b/src/content/docs/it/basics/client-side-rendering.mdx index 682269b6c4f44..b484efd1c0d62 100644 --- a/src/content/docs/it/basics/client-side-rendering.mdx +++ b/src/content/docs/it/basics/client-side-rendering.mdx @@ -148,7 +148,7 @@ Può scegliere di implementare uno qualsiasi dei seguenti tipi di captcha quando Vari framework sono stati integrati con Procaptcha. Può trovare la documentazione per ciascun framework qui sotto: -- [Integrazione React](/it/framework-integrations/angular-integration/) +- [Integrazione React](/it/framework-integrations/react-integration/) - [Integrazione Vue](/it/framework-integrations/vue-integration/) - [Integrazione Angular](/it/framework-integrations/angular-integration/) - [Integrazione Svelte](/it/framework-integrations/svelte-integration/) diff --git a/src/content/docs/it/basics/invisible-captcha.mdx b/src/content/docs/it/basics/invisible-captcha.mdx index a2f333d83a460..0f57bdd2073b3 100644 --- a/src/content/docs/it/basics/invisible-captcha.mdx +++ b/src/content/docs/it/basics/invisible-captcha.mdx @@ -11,7 +11,7 @@ Il CAPTCHA invisibile è attualmente in **beta**. Funzionalità e comportamento ::: :::note[Restrizione Tier] -Il CAPTCHA invisibile è disponibile solo per gli utenti dei tier **Pro ed Enterprise**. Gli utenti del tier gratuito non possono accedere a questa funzionalità. +Il CAPTCHA invisibile è disponibile solo per gli utenti dei tier **Professional ed Enterprise**. Gli utenti del tier gratuito non possono accedere a questa funzionalità. ::: ## Panoramica @@ -219,18 +219,18 @@ const response = await fetch('https://api.prosopo.io/siteverify', { }, body: JSON.stringify({ secret: 'your_secret_key', - response: token, // Token from Procaptcha callback - remoteip: userIP // Optional + token: token, // Token from Procaptcha callback + ip: userIP // Optional }) }); const result = await response.json(); -if (result.success) { +if (result.verified) { // Procaptcha verified successfully console.log('Verification successful'); } else { // Verification failed - console.log('Verification failed:', result['error-codes']); + console.log('Verification failed:', result.status); } ``` diff --git a/src/content/docs/it/basics/server-side-verification.mdx b/src/content/docs/it/basics/server-side-verification.mdx index b68cc4011748e..5827f0f94a95c 100644 --- a/src/content/docs/it/basics/server-side-verification.mdx +++ b/src/content/docs/it/basics/server-side-verification.mdx @@ -90,7 +90,8 @@ async function verifyToken(token) { headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ secret: 'your_secret_key', token }), }); - return response.json().verified || false; // Return verified field, default to false + const data = await response.json(); + return data.verified || false; // Return verified field, default to false } ``` @@ -187,7 +188,8 @@ npm install @prosopo/server Per verificare la risposta di un utente utilizzando JavaScript / TypeScript, importi semplicemente la funzione `verify` da `@prosopo/server` e le passi i dati POST `procaptcha-response`. I tipi possono essere importati da `@prosopo/types`. ```typescript -import {ProsopoServer} from '@prosopo/server' +import {ProsopoServer, getServerConfig} from '@prosopo/server' +import {getPair} from '@prosopo/keyring' import {ApiParams} from '@prosopo/types' ... @@ -198,10 +200,13 @@ const payload = JSON.parse(event.body) const procaptchaResponse = payload[ApiParams.procaptchaResponse] // initialise the `ProsopoServer` class -const prosopoServer = new ProsopoServer(config) +const config = getServerConfig() +const pair = getPair(process.env.PROSOPO_SITE_PRIVATE_KEY, config.account.address) +const prosopoServer = new ProsopoServer(config, pair) // check if the captcha response is verified -if (await prosopoServer.isVerified(procaptchaResponse)) { +const result = await prosopoServer.isVerified(procaptchaResponse) +if (result.verified) { // perform CAPTCHA protected action } ``` diff --git a/src/content/docs/it/welcome/index.mdx b/src/content/docs/it/welcome/index.mdx index 26decc19802d2..9d0285e3733f3 100644 --- a/src/content/docs/it/welcome/index.mdx +++ b/src/content/docs/it/welcome/index.mdx @@ -26,7 +26,7 @@ Può visualizzare un esempio end-to-end di come implementare Procaptcha in una s Vari framework sono stati integrati con Procaptcha. Può trovare la documentazione per ciascun framework qui sotto: -- [Integrazione React](/it/framework-integrations/angular-integration/) +- [Integrazione React](/it/framework-integrations/react-integration/) - [Integrazione Vue](/it/framework-integrations/vue-integration/) - [Integrazione Angular](/it/framework-integrations/angular-integration/) - [Integrazione Svelte](/it/framework-integrations/svelte-integration/) diff --git a/src/content/docs/pt-br/basics/client-side-rendering.mdx b/src/content/docs/pt-br/basics/client-side-rendering.mdx index 65c8eb242195b..12eb0aa3e0a47 100644 --- a/src/content/docs/pt-br/basics/client-side-rendering.mdx +++ b/src/content/docs/pt-br/basics/client-side-rendering.mdx @@ -158,7 +158,7 @@ Você pode escolher implementar qualquer um dos seguintes tipos de captcha ao re Vários frameworks foram integrados com o Procaptcha. Você pode encontrar a documentação para cada framework abaixo: -- [Integração React](/pt-br/framework-integrations/angular-integration/) +- [Integração React](/pt-br/framework-integrations/react-integration/) - [Integração Vue](/pt-br/framework-integrations/vue-integration/) - [Integração Angular](/pt-br/framework-integrations/angular-integration/) - [Integração Svelte](/pt-br/framework-integrations/svelte-integration/) diff --git a/src/content/docs/pt-br/basics/invisible-captcha.mdx b/src/content/docs/pt-br/basics/invisible-captcha.mdx index ad8def88f296f..38641ff892e6a 100644 --- a/src/content/docs/pt-br/basics/invisible-captcha.mdx +++ b/src/content/docs/pt-br/basics/invisible-captcha.mdx @@ -11,7 +11,7 @@ O CAPTCHA Invisível está atualmente em **beta**. Recursos e comportamento pode ::: :::note[Restrição de Nível] -O CAPTCHA Invisível está disponível apenas para usuários dos níveis **Pro e Enterprise**. Usuários do nível gratuito não podem acessar este recurso. +O CAPTCHA Invisível está disponível apenas para usuários dos níveis **Professional e Enterprise**. Usuários do nível gratuito não podem acessar este recurso. ::: ## Visão Geral @@ -219,18 +219,18 @@ const response = await fetch('https://api.prosopo.io/siteverify', { }, body: JSON.stringify({ secret: 'your_secret_key', - response: token, // Token do callback Procaptcha - remoteip: userIP // Opcional + token: token, // Token do callback Procaptcha + ip: userIP // Opcional }) }); const result = await response.json(); -if (result.success) { +if (result.verified) { // Procaptcha verificado com sucesso console.log('Verificação bem-sucedida'); } else { // Falha na verificação - console.log('Falha na verificação:', result['error-codes']); + console.log('Falha na verificação:', result.status); } ``` diff --git a/src/content/docs/pt-br/basics/server-side-verification.mdx b/src/content/docs/pt-br/basics/server-side-verification.mdx index 5d79102f71117..08867baad2ea0 100644 --- a/src/content/docs/pt-br/basics/server-side-verification.mdx +++ b/src/content/docs/pt-br/basics/server-side-verification.mdx @@ -91,7 +91,8 @@ async function verifyToken(token) { headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ secret: 'your_secret_key', token }), }); - return response.json().verified || false; // Retornar campo verified, padrão false + const data = await response.json(); + return data.verified || false; // Retornar campo verified, padrão false } ``` @@ -189,7 +190,8 @@ Para verificar a resposta de um usuário usando JavaScript / TypeScript, simples os dados POST `procaptcha-response`. Os tipos podem ser importados de `@prosopo/types`. ```typescript -import {ProsopoServer} from '@prosopo/server' +import {ProsopoServer, getServerConfig} from '@prosopo/server' +import {getPair} from '@prosopo/keyring' import {ApiParams} from '@prosopo/types' ... @@ -200,10 +202,13 @@ const payload = JSON.parse(event.body) const procaptchaResponse = payload[ApiParams.procaptchaResponse] // inicializar a classe `ProsopoServer` -const prosopoServer = new ProsopoServer(config) +const config = getServerConfig() +const pair = getPair(process.env.PROSOPO_SITE_PRIVATE_KEY, config.account.address) +const prosopoServer = new ProsopoServer(config, pair) // verificar se a resposta do captcha está verificada -if (await prosopoServer.isVerified(procaptchaResponse)) { +const result = await prosopoServer.isVerified(procaptchaResponse) +if (result.verified) { // executar ação protegida por CAPTCHA } ``` diff --git a/src/content/docs/pt-br/welcome/index.mdx b/src/content/docs/pt-br/welcome/index.mdx index 38f44683055e0..a41ff298f6a4f 100644 --- a/src/content/docs/pt-br/welcome/index.mdx +++ b/src/content/docs/pt-br/welcome/index.mdx @@ -30,7 +30,7 @@ de como executar os exemplos estão na documentação nos links anteriores. Vários frameworks foram integrados com Procaptcha. Você pode encontrar a documentação para cada framework abaixo: -- [Integração com React](/pt-br/framework-integrations/angular-integration/) +- [Integração com React](/pt-br/framework-integrations/react-integration/) - [Integração com Vue](/pt-br/framework-integrations/vue-integration/) - [Integração com Angular](/pt-br/framework-integrations/angular-integration/) - [Integração com Svelte](/pt-br/framework-integrations/svelte-integration/) From e5b39878c6f5665d92f9145d10e6a68bf64cd2ae Mon Sep 17 00:00:00 2001 From: Chris Taylor Date: Tue, 4 Aug 2026 14:22:13 +0100 Subject: [PATCH 04/10] docs: add Prosopo Protect (Edge) section with Cloudflare Worker guide MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a new sidebar section that documents the two edge integrations (Lambda@Edge, Cloudflare Workers) and a full deploy guide for the CF Worker so users can go from zero to a live worker without leaving the docs. - src/content/docs/en/protect-edge/index.mdx — overview of the edge integration model, decision tree, correlation, and what lives in @prosopo/protect-edge-core vs the platform adapters. - src/content/docs/en/protect-edge/cloudflare-worker.mdx — install, seed-settings, deploy, verify, npm scripts, TLS caveats, proxy/VPN/datacenter blocking, troubleshooting. - src/i18n/en/nav.ts — new "Prosopo Protect (Edge)" nav section. Verified with `npm run check` (0 errors) and `npm run build` (both pages render at /en/protect-edge/ and /en/protect-edge/cloudflare-worker/). Co-Authored-By: Claude Opus 4.7 (1M context) --- .../en/protect-edge/cloudflare-worker.mdx | 189 ++++++++++++++++++ src/content/docs/en/protect-edge/index.mdx | 44 ++++ src/i18n/en/nav.ts | 11 + 3 files changed, 244 insertions(+) create mode 100644 src/content/docs/en/protect-edge/cloudflare-worker.mdx create mode 100644 src/content/docs/en/protect-edge/index.mdx diff --git a/src/content/docs/en/protect-edge/cloudflare-worker.mdx b/src/content/docs/en/protect-edge/cloudflare-worker.mdx new file mode 100644 index 0000000000000..c1dcd69c2ec14 --- /dev/null +++ b/src/content/docs/en/protect-edge/cloudflare-worker.mdx @@ -0,0 +1,189 @@ +--- +title: Cloudflare Worker +description: Deploy Prosopo Protect as a Cloudflare Worker so proxy / VPN / datacenter / tor traffic is blocked at Cloudflare's edge before it reaches your origin. +i18nReady: true +--- + +`@prosopo/cloudflare-worker` is a Cloudflare Workers port of the Prosopo Protect edge integration. It's the CF-native counterpart of `@prosopo/lambda-edge` and shares the decision tree, envelope shape, block/challenge HTML, and per-isolate caches via [`@prosopo/protect-edge-core`](/protect-edge/). + +The worker sits in front of an origin (or, in the demo, a static assets bundle) and runs Prosopo's Protect logic on **every** request. On `allow` it forwards; on `block` / `challenge` / `no-session` it returns immediately without touching the origin. + +## Prerequisites + +- A Cloudflare account with **Workers Scripts: Read + Edit** on the API token you'll use. +- A Prosopo site key (SS58 address, e.g. `5HSuC1s…`) registered as a Protect instance on the Bumblebee you're targeting (see [Seed a Protect instance](#seed-a-protect-instance)). +- Node 24+, `npx`, `wrangler` (installed as a dev dep of the package). + +## Configuration model + +Cloudflare Workers takes a different approach from Lambda@Edge: + +| Value | Lambda@Edge | Cloudflare Worker | +|--|--|--| +| `BUMBLEBEE_URL` | Inlined at build time via vite `define` (Lambda@Edge rejects env vars). | `wrangler.toml` `[vars]`, overridable per deploy with `--var`. | +| `CLIENT_JWT` (secret) | Inlined at build time via vite `define`. | Runtime binding uploaded via `wrangler secret put`. | +| Site key (client HTML) | Not applicable — Lambda@Edge doesn't serve the demo HTML. | `sed` into `demo-site/index.html` before bundling. | + +The CF Worker bundle itself is site-agnostic. Client-specific configuration lives in Cloudflare's config store, not in the JS. + +## Install & type-check + +```bash +cd protect/packages/cloudflare-worker +npm install +npm run build:tsc # cascades to @prosopo/protect-edge-core via tsc --build +``` + +## Seed a Protect instance + +Before the worker's `/api/access-check` and `/api/jti/{jti}` calls will succeed for your site key, Bumblebee needs a `ProtectClientSettings` row for it. The `scripts/seed-settings.sh` script handles both `POST /api/settings` (the client settings blob) and `POST /api/access-rules` (the tiered IP-category rules the edge consults on no-cookie requests). + +```bash +BUMBLEBEE_URL=https://protect.prosopo.io \ +SITE_KEY=5HSuC1s1VvSXq17bNu1Fp2Z2P6Vs8CKZNZB2bAqHzS4uhaTT \ +SITE_SURI="unusual bulb melt vanish ice correct myth ribbon second tunnel ride sadness" \ +CNAME=your-worker-subdomain.workers.dev \ + bash scripts/seed-settings.sh +``` + +The seed script signs an admin JWT with the SURI in `ADMIN_SURI` (defaults to the Prosopo prod admin mnemonic). To point at a non-prod Bumblebee, pass a matching `ADMIN_SURI`. + +To skip inserting the tiered IP-category rules (redundant on a Bumblebee that folds `settings.ipCategoryRules` into `access-check`), set `SKIP_ACCESS_RULES=1`. + +## Deploy + +```bash +export CLOUDFLARE_API_TOKEN= +export CLOUDFLARE_ACCOUNT_ID= + +# One-off: push the CLIENT_JWT secret (persists across deploys). +npm run deploy:secrets + +# Deploy — build:tsc runs first, then wrangler. +npm run deploy +``` + +To override `BUMBLEBEE_URL` for a specific deploy without editing `wrangler.toml`: + +```bash +npm run build:tsc \ + && npx wrangler deploy --var BUMBLEBEE_URL:https://protect.twickets.live +``` + +To produce a bundle for inspection without deploying: + +```bash +npm run bundle # → dist/bundle/index.js + source map +``` + +## npm scripts + +Same shape as `@prosopo/lambda-edge`: + +| Script | What it does | +|--|--| +| `clean` | Removes `dist/`, `.wrangler/`, `tsconfig.tsbuildinfo`. | +| `build:tsc` | Type-check via `tsc --build` (walks to `protect-edge-core`). | +| `typecheck` | `tsc --build --noEmit`. | +| `bundle` | `wrangler deploy --dry-run --outdir dist/bundle` — bundle to disk, no upload. | +| `deploy` | `build:tsc` then `wrangler deploy`. Loads `.env` if present. | +| `deploy:secrets` | Runs `scripts/upload-secrets.sh` — mints a `CLIENT_JWT` and uploads it as a wrangler secret. | +| `dev` | `wrangler dev` — local dev server against your workers.dev subdomain. | +| `tail` | `wrangler tail` — live request logs from the deployed worker. | +| `seed-settings` | Runs `scripts/seed-settings.sh` — seeds Bumblebee with settings + access rules. | +| `issue-client-jwt` | Prints a fresh 1-year CLIENT_JWT signed with `SITE_SURI`. | +| `issue-admin-jwt` | Prints a 1-hour admin JWT signed with `ADMIN_SURI`. | + +## Verify the deploy + +Curl the worker directly — no session cookie, HTML `Accept` should pass through so the Protect bundle can load; JSON `Accept` should be 401 with `X-Prosopo-Status: no-session`: + +```bash +WORKER=https://your-worker.workers.dev + +# HTML — should serve the origin / demo (200) +curl -sD - -H "Accept: text/html" -H "User-Agent: Mozilla/5.0" "$WORKER/" | head + +# JSON, no cookie — should 401 with no-session status +curl -sD - -H "Accept: application/json" "$WORKER/api/anything" | head +# HTTP/2 401 +# x-prosopo-status: no-session +# x-prosopo-request-id: + +# From a proxy / VPN / datacenter IP — should 403 with the branded interstitial +curl -sD - -H "Accept: text/html" -x http://your-proxy:port "$WORKER/" | head +# HTTP/2 403 +# x-prosopo-decision: block +``` + +`X-Prosopo-Request-Id` is echoed on every response and written to Bumblebee's `verdict_log` row — grep from a support ticket straight to the verdict. + +## What the worker is protecting + +Out of the box the worker serves a small demo page from `demo-site/` via wrangler's ASSETS binding. To point it at a real origin, replace the `env.ASSETS.fetch(...)` call at the end of `src/index.ts` with `fetch(rewrittenUrl, request)` and set the upstream in `wrangler.toml`. The Protect decision tree above the fetch is unchanged. + +The `[assets]` block in `wrangler.toml` sets `run_worker_first = true` so the worker runs before Cloudflare serves any static file — without that, `/` would be served from cache and Protect would never see the request. + +## Configuring proxy / VPN / datacenter blocking + +Two rule stores independently feed the no-cookie edge path: + +- **`settings.ipCategoryRules`** — set via `POST /api/settings` (the seed script writes this). Recognised categories: `tor`, `datacenter`, `proxy`, `vpn`, `abuser`, `mobile`, `crawler`. +- **Tiered access rules** — set via `POST /api/access-rules`. Supports the full tier set (IP CIDR, ASN, IP category, country, UA substring, JA4, JA4+IP) at either `client` or `global` scope. + +On any Bumblebee build with the settings-fold, either store fires the edge block. The effective verdict is the most restrictive of the two (Block > Challenge > Allow). See the `combine_matches` unit tests in `crates/bumblebee/src/http/routes/access_check.rs` for the precedence rules. + +For a "just block proxies" deploy, seed: + +```bash +# in scripts/seed-settings.sh, in the /api/settings payload: +"ipCategoryRules": { + "proxy": "block", + "vpn": "block", + "tor": "block", + "datacenter": "block", + "abuser": "block" +} +``` + +## TLS caveats + +Cloudflare Workers doesn't expose a `rejectUnauthorized: false` option — if the Bumblebee host's certificate is expired or self-signed, `fetch` returns HTTP 526 and Prosopo can't be reached. Options: + +- Use a Bumblebee hostname with a valid cert (`bumblebee1.prosopo.io` is a fallback for the prod BB). +- Renew the cert on `protect.prosopo.io` (managed via `protect/ansible/renew_bb1_certs.sh`). + +Lambda@Edge doesn't have this constraint — it can skip TLS verification per `protect/packages/lambda-edge/src/tlsOptions.ts`. + +## Troubleshooting + +- **`wrangler deploy` returns `Authentication error [code: 10000]`** — the token lacks **Workers Scripts: Read + Edit**. "Edit" alone isn't enough; wrangler needs both. +- **`Unknown site` from `/api/access-check`** — no `ProtectClientSettings` row for the site key. Run `seed-settings.sh` first. +- **Bundle deploys but every request 401s** — `CLIENT_JWT` isn't set or has expired. Re-run `npm run deploy:secrets` and redeploy. +- **HTTP 526 in `wrangler tail`** — Bumblebee cert expired or invalid; see [TLS caveats](#tls-caveats). +- **`/api/probe` returns HTML `Access Denied` instead of JSON** — the endpoint's `path_type` is `html` (either the site default or an endpoint rule). Set the site's `defaultPathType` to `json` and/or add an endpoint rule for `/` that forces `html` on the root only. + +## Package structure + +``` +protect/packages/cloudflare-worker/ + demo-site/ + index.html # served via wrangler ASSETS binding + scripts/ + issue-admin-jwt.ts # mints admin JWT for /api/settings + /api/access-rules + issue-client-jwt.ts # mints CLIENT_JWT for the worker to authenticate to BB + seed-settings.sh # seeds Bumblebee (settings + tiered access rules) + upload-secrets.sh # mints + uploads CLIENT_JWT as a wrangler secret + src/ + index.ts # fetch handler, runs Protect then delegates to ASSETS + accessCheck.ts # /api/access-check client (no-cookie path) + verdict.ts # /api/jti/{jti} client (cookied path) + bbRequest.ts # buildEnvelope (Fetch API-specific) + session.ts, cookies.ts # prosopo_session cookie parsing + headers.ts # cf-connecting-ip, cf-ray, Accept: text/html detection + corsHeaders.ts # credentialed CORS Headers mutation + responses.ts # 401 / 403 / interstitial / challenge Response builders + verdictResponse.ts # JTI verdict → Response mapping + types.ts # WorkerEnv bindings + wrangler.toml # worker config, [vars], [assets], run_worker_first +``` diff --git a/src/content/docs/en/protect-edge/index.mdx b/src/content/docs/en/protect-edge/index.mdx new file mode 100644 index 0000000000000..90c25ff06d6b2 --- /dev/null +++ b/src/content/docs/en/protect-edge/index.mdx @@ -0,0 +1,44 @@ +--- +title: Prosopo Protect at the Edge +description: Deploy Prosopo Protect in front of your site with a Cloudflare Worker or AWS Lambda@Edge. Verdict lookups run at your CDN's edge so bots are blocked before they reach the origin. +i18nReady: true +--- + +Prosopo Protect can run at the edge of your CDN so blocked requests never reach the origin. Two integrations are supported today: + +- **Cloudflare Workers** — [`@prosopo/cloudflare-worker`](/protect-edge/cloudflare-worker). +- **AWS Lambda@Edge** — `@prosopo/lambda-edge`, deployed via the Serverless Framework in front of a CloudFront distribution. + +Both integrations share the same decision tree and talk to the same Bumblebee (`/api/access-check`, `/api/jti/{jti}`) endpoints. The shared logic (envelope shape, block/challenge HTML, path-type and passive caches, branding sanitisation) lives in `@prosopo/protect-edge-core` so a change made in one integration lands in the other. + +## What the edge integration does + +On every request that reaches your CDN: + +1. If the request carries a valid `prosopo_session` cookie, the edge looks up the JTI verdict at Bumblebee. A `block` or `challenge` verdict is returned to the user immediately (branded interstitial for HTML, JSON for XHR/fetch); an `allow` verdict passes the request through to your origin. +2. If the request has no cookie, the edge calls `/api/access-check` with the caller's IP, User-Agent, headers, and path. Bumblebee evaluates access rules (IP CIDR, ASN, IP category, country, UA substring, JA4) against the request and returns the effective decision: + - `allow` with source `no_cookie_html_bootstrap` — HTML page loads so the Protect telemetry bundle can create a session. + - `block` with source `no_session_401` — 401 with `X-Prosopo-Status: no-session` (recoverable — the client can call `/session/init`). + - `block` with source `access_rule_nocookie` — 403 (an IP-category / ASN / etc. rule fired). Never reaches the origin. + - `challenge` — HTML client sees a captcha interstitial; JSON client sees the same block behaviour (no session, no captcha surface to render into). + +## Correlation + +Every edge request is stamped with an `X-Prosopo-Request-Id` header (`cf-ray` on Cloudflare, `cf.config.requestId` on Lambda@Edge). The same ID is written to Bumblebee's `verdict_log` row and echoed back on the response, so you can grep from a support ticket straight to the verdict that fired. + +## What lives where + +| Concern | Where | +|--|--| +| Decision tree | Each adapter (`lambda-edge`, `cloudflare-worker`) | +| Envelope shape, `bodyWithEnvelope`, `envelopeAsHeaders` | `@prosopo/protect-edge-core` | +| Block / challenge HTML + branding sanitisers | `@prosopo/protect-edge-core` | +| `passiveCache`, `pathTypeCache` | `@prosopo/protect-edge-core` | +| CloudFront-shaped headers / responses | `lambda-edge` | +| Fetch API `Headers` / `Response` shape, `wrangler.toml` | `cloudflare-worker` | +| Bumblebee routes (`/api/access-check`, `/api/jti/{jti}`, `/api/settings`, `/api/access-rules`) | `crates/bumblebee` | + +## Next steps + +- **Cloudflare Workers** — [Deploy the CF Worker](/protect-edge/cloudflare-worker). +- **AWS Lambda@Edge** — same shape, deployed via `serverless deploy` in front of a CloudFront distribution. Environment variables (`BUMBLEBEE_URL`, `CLIENT_JWT`) are inlined at build time via vite because Lambda@Edge rejects functions that carry runtime env vars. diff --git a/src/i18n/en/nav.ts b/src/i18n/en/nav.ts index 0e6aea8a13ebf..ed391894ebd7d 100644 --- a/src/i18n/en/nav.ts +++ b/src/i18n/en/nav.ts @@ -96,6 +96,17 @@ export default [ slug: 'advanced/audit', key: 'advanced/audit', }, + {text: 'Prosopo Protect (Edge)', header: true, type: 'learn', key: 'protect-edge'}, + { + text: 'Overview', + slug: 'protect-edge/', + key: 'protect-edge/', + }, + { + text: 'Cloudflare Worker', + slug: 'protect-edge/cloudflare-worker', + key: 'protect-edge/cloudflare-worker', + }, {text: 'Framework integrations', header: true, type: 'learn', key: 'framework-integrations'}, { text: 'Angular Integration', From 62037dbffe836a071020cbe0f0dd2274a2e539b3 Mon Sep 17 00:00:00 2001 From: Chris Taylor Date: Tue, 4 Aug 2026 14:52:58 +0100 Subject: [PATCH 05/10] docs(protect-edge): add Lambda@Edge guide and re-scope for external readers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - New /protect-edge/lambda-edge/ page: prereqs, bundle-time config model (Lambda@Edge can't carry env vars, so BUMBLEBEE_URL and CLIENT_JWT are inlined at bundle time), deploy via Serverless Framework, npm scripts, verify, constraints (region, runtime, memory, time, bundle size), and troubleshooting. - Rewrites the overview and Cloudflare Worker pages to drop internal names and repo internals — no more "Bumblebee", "@prosopo/protect- edge-core", package-structure trees, or ansible paths. Site setup is framed as a dashboard/account-manager task rather than a script the client runs with the admin mnemonic. - Nav entry for the new Lambda@Edge page. Verified with `npm run check` (0 errors) and `npm run build` (326 pages built, all three protect-edge pages routed). Co-Authored-By: Claude Opus 4.7 (1M context) --- .../en/protect-edge/cloudflare-worker.mdx | 137 +++++------------ src/content/docs/en/protect-edge/index.mdx | 44 +++--- .../docs/en/protect-edge/lambda-edge.mdx | 144 ++++++++++++++++++ src/i18n/en/nav.ts | 5 + 4 files changed, 206 insertions(+), 124 deletions(-) create mode 100644 src/content/docs/en/protect-edge/lambda-edge.mdx diff --git a/src/content/docs/en/protect-edge/cloudflare-worker.mdx b/src/content/docs/en/protect-edge/cloudflare-worker.mdx index c1dcd69c2ec14..4732f31f177c1 100644 --- a/src/content/docs/en/protect-edge/cloudflare-worker.mdx +++ b/src/content/docs/en/protect-edge/cloudflare-worker.mdx @@ -1,55 +1,38 @@ --- title: Cloudflare Worker -description: Deploy Prosopo Protect as a Cloudflare Worker so proxy / VPN / datacenter / tor traffic is blocked at Cloudflare's edge before it reaches your origin. +description: Deploy Prosopo Protect as a Cloudflare Worker so proxy, VPN, datacenter and Tor traffic is blocked at Cloudflare's edge before it reaches your origin. i18nReady: true --- -`@prosopo/cloudflare-worker` is a Cloudflare Workers port of the Prosopo Protect edge integration. It's the CF-native counterpart of `@prosopo/lambda-edge` and shares the decision tree, envelope shape, block/challenge HTML, and per-isolate caches via [`@prosopo/protect-edge-core`](/protect-edge/). - -The worker sits in front of an origin (or, in the demo, a static assets bundle) and runs Prosopo's Protect logic on **every** request. On `allow` it forwards; on `block` / `challenge` / `no-session` it returns immediately without touching the origin. +The Prosopo Protect Cloudflare Worker sits in front of your origin and runs Protect's decision tree on every incoming request. On `allow` it forwards the request; on `block` / `challenge` it returns immediately without touching your origin. ## Prerequisites - A Cloudflare account with **Workers Scripts: Read + Edit** on the API token you'll use. -- A Prosopo site key (SS58 address, e.g. `5HSuC1s…`) registered as a Protect instance on the Bumblebee you're targeting (see [Seed a Protect instance](#seed-a-protect-instance)). -- Node 24+, `npx`, `wrangler` (installed as a dev dep of the package). +- A Prosopo site key (SS58 address, e.g. `5HSuC1s…`) with Protect enabled on your [Prosopo dashboard](https://portal.prosopo.io/) or by your Prosopo account manager. +- A `CLIENT_JWT` for your site — a long-lived sr25519 JWT signed by your site's key. Your Prosopo dashboard can generate this, or you can mint one locally with `npm run issue-client-jwt` (see below). +- Node 24+ and `npx`. ## Configuration model -Cloudflare Workers takes a different approach from Lambda@Edge: +Cloudflare Workers take a different approach from Lambda@Edge: -| Value | Lambda@Edge | Cloudflare Worker | +| Value | How it's supplied | Where it lives | |--|--|--| -| `BUMBLEBEE_URL` | Inlined at build time via vite `define` (Lambda@Edge rejects env vars). | `wrangler.toml` `[vars]`, overridable per deploy with `--var`. | -| `CLIENT_JWT` (secret) | Inlined at build time via vite `define`. | Runtime binding uploaded via `wrangler secret put`. | -| Site key (client HTML) | Not applicable — Lambda@Edge doesn't serve the demo HTML. | `sed` into `demo-site/index.html` before bundling. | +| `BUMBLEBEE_URL` (Protect API base URL) | `wrangler.toml` `[vars]` — public, overridable per deploy with `--var`. | Cloudflare Workers env binding. | +| `CLIENT_JWT` (secret) | `wrangler secret put CLIENT_JWT`. | Cloudflare Workers secret store. | +| Your site key | Baked into your HTML — the Protect telemetry bundle reads it as `data-site-key`. | Your static assets / origin HTML. | -The CF Worker bundle itself is site-agnostic. Client-specific configuration lives in Cloudflare's config store, not in the JS. +The worker bundle itself is site-agnostic. Client-specific configuration lives in Cloudflare's config store. ## Install & type-check ```bash cd protect/packages/cloudflare-worker npm install -npm run build:tsc # cascades to @prosopo/protect-edge-core via tsc --build -``` - -## Seed a Protect instance - -Before the worker's `/api/access-check` and `/api/jti/{jti}` calls will succeed for your site key, Bumblebee needs a `ProtectClientSettings` row for it. The `scripts/seed-settings.sh` script handles both `POST /api/settings` (the client settings blob) and `POST /api/access-rules` (the tiered IP-category rules the edge consults on no-cookie requests). - -```bash -BUMBLEBEE_URL=https://protect.prosopo.io \ -SITE_KEY=5HSuC1s1VvSXq17bNu1Fp2Z2P6Vs8CKZNZB2bAqHzS4uhaTT \ -SITE_SURI="unusual bulb melt vanish ice correct myth ribbon second tunnel ride sadness" \ -CNAME=your-worker-subdomain.workers.dev \ - bash scripts/seed-settings.sh +npm run build:tsc ``` -The seed script signs an admin JWT with the SURI in `ADMIN_SURI` (defaults to the Prosopo prod admin mnemonic). To point at a non-prod Bumblebee, pass a matching `ADMIN_SURI`. - -To skip inserting the tiered IP-category rules (redundant on a Bumblebee that folds `settings.ipCategoryRules` into `access-check`), set `SKIP_ACCESS_RULES=1`. - ## Deploy ```bash @@ -63,14 +46,14 @@ npm run deploy:secrets npm run deploy ``` -To override `BUMBLEBEE_URL` for a specific deploy without editing `wrangler.toml`: +To override the Protect API URL for a specific deploy without editing `wrangler.toml`: ```bash npm run build:tsc \ - && npx wrangler deploy --var BUMBLEBEE_URL:https://protect.twickets.live + && npx wrangler deploy --var BUMBLEBEE_URL:https://protect.your-domain.com ``` -To produce a bundle for inspection without deploying: +To produce a bundle for inspection or hand-off without deploying: ```bash npm run bundle # → dist/bundle/index.js + source map @@ -78,30 +61,33 @@ npm run bundle # → dist/bundle/index.js + source map ## npm scripts -Same shape as `@prosopo/lambda-edge`: - | Script | What it does | |--|--| | `clean` | Removes `dist/`, `.wrangler/`, `tsconfig.tsbuildinfo`. | -| `build:tsc` | Type-check via `tsc --build` (walks to `protect-edge-core`). | +| `build:tsc` | Type-check via `tsc --build`. | | `typecheck` | `tsc --build --noEmit`. | | `bundle` | `wrangler deploy --dry-run --outdir dist/bundle` — bundle to disk, no upload. | | `deploy` | `build:tsc` then `wrangler deploy`. Loads `.env` if present. | -| `deploy:secrets` | Runs `scripts/upload-secrets.sh` — mints a `CLIENT_JWT` and uploads it as a wrangler secret. | -| `dev` | `wrangler dev` — local dev server against your workers.dev subdomain. | +| `deploy:secrets` | Mints a `CLIENT_JWT` and uploads it as a wrangler secret. | +| `dev` | `wrangler dev` — local dev server. | | `tail` | `wrangler tail` — live request logs from the deployed worker. | -| `seed-settings` | Runs `scripts/seed-settings.sh` — seeds Bumblebee with settings + access rules. | -| `issue-client-jwt` | Prints a fresh 1-year CLIENT_JWT signed with `SITE_SURI`. | -| `issue-admin-jwt` | Prints a 1-hour admin JWT signed with `ADMIN_SURI`. | +| `issue-client-jwt` | Prints a fresh 1-year `CLIENT_JWT` signed with `SITE_SURI`. | + +`SITE_SURI` is your site's sr25519 secret (mnemonic + optional derivation path) — the same value your site was created with. Export it before running the JWT-issuing script: + +```bash +SITE_SURI="unusual bulb melt vanish ice correct myth ribbon second tunnel ride sadness" \ + npm run issue-client-jwt +``` ## Verify the deploy -Curl the worker directly — no session cookie, HTML `Accept` should pass through so the Protect bundle can load; JSON `Accept` should be 401 with `X-Prosopo-Status: no-session`: +From an unproxied connection, HTML requests should pass through so the Protect script can load; API requests without a session should return 401 with `X-Prosopo-Status: no-session`. ```bash WORKER=https://your-worker.workers.dev -# HTML — should serve the origin / demo (200) +# HTML — should serve your origin (200) curl -sD - -H "Accept: text/html" -H "User-Agent: Mozilla/5.0" "$WORKER/" | head # JSON, no cookie — should 401 with no-session status @@ -110,80 +96,37 @@ curl -sD - -H "Accept: application/json" "$WORKER/api/anything" | head # x-prosopo-status: no-session # x-prosopo-request-id: -# From a proxy / VPN / datacenter IP — should 403 with the branded interstitial +# From a proxy or VPN — should 403 with the branded interstitial curl -sD - -H "Accept: text/html" -x http://your-proxy:port "$WORKER/" | head # HTTP/2 403 # x-prosopo-decision: block ``` -`X-Prosopo-Request-Id` is echoed on every response and written to Bumblebee's `verdict_log` row — grep from a support ticket straight to the verdict. +`X-Prosopo-Request-Id` is echoed on every response and written to Protect's verdict audit log — grep from a support ticket straight to the verdict. ## What the worker is protecting -Out of the box the worker serves a small demo page from `demo-site/` via wrangler's ASSETS binding. To point it at a real origin, replace the `env.ASSETS.fetch(...)` call at the end of `src/index.ts` with `fetch(rewrittenUrl, request)` and set the upstream in `wrangler.toml`. The Protect decision tree above the fetch is unchanged. +Out of the box the worker ships with a small demo page served via wrangler's ASSETS binding. To point it at a real origin, replace the `env.ASSETS.fetch(...)` call at the end of `src/index.ts` with a `fetch(rewrittenUrl, request)` against your upstream and set the origin host in `wrangler.toml`. The Protect decision tree above the fetch is unchanged. The `[assets]` block in `wrangler.toml` sets `run_worker_first = true` so the worker runs before Cloudflare serves any static file — without that, `/` would be served from cache and Protect would never see the request. -## Configuring proxy / VPN / datacenter blocking - -Two rule stores independently feed the no-cookie edge path: +## Configuring proxy, VPN and datacenter blocking -- **`settings.ipCategoryRules`** — set via `POST /api/settings` (the seed script writes this). Recognised categories: `tor`, `datacenter`, `proxy`, `vpn`, `abuser`, `mobile`, `crawler`. -- **Tiered access rules** — set via `POST /api/access-rules`. Supports the full tier set (IP CIDR, ASN, IP category, country, UA substring, JA4, JA4+IP) at either `client` or `global` scope. +Two independent rule sources feed the no-cookie edge path: -On any Bumblebee build with the settings-fold, either store fires the edge block. The effective verdict is the most restrictive of the two (Block > Challenge > Allow). See the `combine_matches` unit tests in `crates/bumblebee/src/http/routes/access_check.rs` for the precedence rules. +- **Site settings** — recognised IP categories: `tor`, `datacenter`, `proxy`, `vpn`, `abuser`, `mobile`, `crawler`. Configure per-category verdicts (`allow` / `challenge` / `block`) on your Prosopo dashboard. +- **Tiered access rules** — set via the Protect API. Support the full rule set: IP CIDR, ASN, IP category, country, User-Agent substring, JA4 TLS fingerprint. Rules can be scoped to your site or applied globally by Prosopo. -For a "just block proxies" deploy, seed: - -```bash -# in scripts/seed-settings.sh, in the /api/settings payload: -"ipCategoryRules": { - "proxy": "block", - "vpn": "block", - "tor": "block", - "datacenter": "block", - "abuser": "block" -} -``` +Either source firing blocks the request at the edge. When both match, the more restrictive verdict wins (`Block > Challenge > Allow`). ## TLS caveats -Cloudflare Workers doesn't expose a `rejectUnauthorized: false` option — if the Bumblebee host's certificate is expired or self-signed, `fetch` returns HTTP 526 and Prosopo can't be reached. Options: - -- Use a Bumblebee hostname with a valid cert (`bumblebee1.prosopo.io` is a fallback for the prod BB). -- Renew the cert on `protect.prosopo.io` (managed via `protect/ansible/renew_bb1_certs.sh`). - -Lambda@Edge doesn't have this constraint — it can skip TLS verification per `protect/packages/lambda-edge/src/tlsOptions.ts`. +Cloudflare Workers doesn't expose a way to skip TLS verification on outbound fetches. If your configured Protect API URL has an invalid or expired certificate, `fetch` returns HTTP 526 and Protect can't be reached. Point `BUMBLEBEE_URL` at a hostname with a valid certificate, or renew the cert on the current host. ## Troubleshooting - **`wrangler deploy` returns `Authentication error [code: 10000]`** — the token lacks **Workers Scripts: Read + Edit**. "Edit" alone isn't enough; wrangler needs both. -- **`Unknown site` from `/api/access-check`** — no `ProtectClientSettings` row for the site key. Run `seed-settings.sh` first. +- **`Unknown site` from `/api/access-check`** — your site key isn't registered with Protect yet. Check your Prosopo dashboard or contact your account manager. - **Bundle deploys but every request 401s** — `CLIENT_JWT` isn't set or has expired. Re-run `npm run deploy:secrets` and redeploy. -- **HTTP 526 in `wrangler tail`** — Bumblebee cert expired or invalid; see [TLS caveats](#tls-caveats). -- **`/api/probe` returns HTML `Access Denied` instead of JSON** — the endpoint's `path_type` is `html` (either the site default or an endpoint rule). Set the site's `defaultPathType` to `json` and/or add an endpoint rule for `/` that forces `html` on the root only. - -## Package structure - -``` -protect/packages/cloudflare-worker/ - demo-site/ - index.html # served via wrangler ASSETS binding - scripts/ - issue-admin-jwt.ts # mints admin JWT for /api/settings + /api/access-rules - issue-client-jwt.ts # mints CLIENT_JWT for the worker to authenticate to BB - seed-settings.sh # seeds Bumblebee (settings + tiered access rules) - upload-secrets.sh # mints + uploads CLIENT_JWT as a wrangler secret - src/ - index.ts # fetch handler, runs Protect then delegates to ASSETS - accessCheck.ts # /api/access-check client (no-cookie path) - verdict.ts # /api/jti/{jti} client (cookied path) - bbRequest.ts # buildEnvelope (Fetch API-specific) - session.ts, cookies.ts # prosopo_session cookie parsing - headers.ts # cf-connecting-ip, cf-ray, Accept: text/html detection - corsHeaders.ts # credentialed CORS Headers mutation - responses.ts # 401 / 403 / interstitial / challenge Response builders - verdictResponse.ts # JTI verdict → Response mapping - types.ts # WorkerEnv bindings - wrangler.toml # worker config, [vars], [assets], run_worker_first -``` +- **HTTP 526 in `wrangler tail`** — the Protect API host's certificate is invalid or expired; see [TLS caveats](#tls-caveats). +- **HTML `Access Denied` served instead of JSON on API paths** — the endpoint's declared content type is HTML. Update the site or endpoint's `defaultPathType` to `json` on your Prosopo dashboard. diff --git a/src/content/docs/en/protect-edge/index.mdx b/src/content/docs/en/protect-edge/index.mdx index 90c25ff06d6b2..7ffa797cb268c 100644 --- a/src/content/docs/en/protect-edge/index.mdx +++ b/src/content/docs/en/protect-edge/index.mdx @@ -1,44 +1,34 @@ --- title: Prosopo Protect at the Edge -description: Deploy Prosopo Protect in front of your site with a Cloudflare Worker or AWS Lambda@Edge. Verdict lookups run at your CDN's edge so bots are blocked before they reach the origin. +description: Deploy Prosopo Protect in front of your site with a Cloudflare Worker or AWS Lambda@Edge. Verdict lookups run at your CDN's edge so bots are blocked before they reach your origin. i18nReady: true --- -Prosopo Protect can run at the edge of your CDN so blocked requests never reach the origin. Two integrations are supported today: +Prosopo Protect can run at the edge of your CDN so blocked and challenged requests never reach your origin. Two integrations are supported today: -- **Cloudflare Workers** — [`@prosopo/cloudflare-worker`](/protect-edge/cloudflare-worker). -- **AWS Lambda@Edge** — `@prosopo/lambda-edge`, deployed via the Serverless Framework in front of a CloudFront distribution. +- **[Cloudflare Workers](/protect-edge/cloudflare-worker)** — deploy alongside your Cloudflare zone. +- **[AWS Lambda@Edge](/protect-edge/lambda-edge)** — deploy in front of a CloudFront distribution. -Both integrations share the same decision tree and talk to the same Bumblebee (`/api/access-check`, `/api/jti/{jti}`) endpoints. The shared logic (envelope shape, block/challenge HTML, path-type and passive caches, branding sanitisation) lives in `@prosopo/protect-edge-core` so a change made in one integration lands in the other. +Both integrations enforce the same policy: they consult the Prosopo Protect API on every incoming request and act on the returned verdict before it reaches your origin. -## What the edge integration does +## How it works -On every request that reaches your CDN: +On every request that hits your CDN: -1. If the request carries a valid `prosopo_session` cookie, the edge looks up the JTI verdict at Bumblebee. A `block` or `challenge` verdict is returned to the user immediately (branded interstitial for HTML, JSON for XHR/fetch); an `allow` verdict passes the request through to your origin. -2. If the request has no cookie, the edge calls `/api/access-check` with the caller's IP, User-Agent, headers, and path. Bumblebee evaluates access rules (IP CIDR, ASN, IP category, country, UA substring, JA4) against the request and returns the effective decision: - - `allow` with source `no_cookie_html_bootstrap` — HTML page loads so the Protect telemetry bundle can create a session. - - `block` with source `no_session_401` — 401 with `X-Prosopo-Status: no-session` (recoverable — the client can call `/session/init`). - - `block` with source `access_rule_nocookie` — 403 (an IP-category / ASN / etc. rule fired). Never reaches the origin. - - `challenge` — HTML client sees a captcha interstitial; JSON client sees the same block behaviour (no session, no captcha surface to render into). +1. **If the request has a `prosopo_session` cookie** the edge fetches the verdict for the session's token from the Prosopo Protect API. `allow` passes through to your origin; `block` returns a branded interstitial (or JSON error for API calls) with no origin fetch; `challenge` serves a captcha interstitial. +2. **If the request has no cookie** the edge asks Protect what to do. Protect evaluates your access-rule set (IP CIDR, ASN, IP category, country, User-Agent, JA4 TLS fingerprint) against the request. Rule matches return `block` or `challenge`; if no rule matches and the path is an HTML SPA shell, the edge lets it through so the Protect script can create a session. + +The blocked-response HTML and challenge interstitial are branded per site via your Prosopo dashboard: logo, colour palette, typography, and message strings all sanitised and rendered server-side by the edge. ## Correlation -Every edge request is stamped with an `X-Prosopo-Request-Id` header (`cf-ray` on Cloudflare, `cf.config.requestId` on Lambda@Edge). The same ID is written to Bumblebee's `verdict_log` row and echoed back on the response, so you can grep from a support ticket straight to the verdict that fired. +Every edge request is stamped with an `X-Prosopo-Request-Id` header that's echoed on the response and written to the verdict audit log. From a support ticket you can grep straight to the verdict that fired. -## What lives where +## What's next -| Concern | Where | -|--|--| -| Decision tree | Each adapter (`lambda-edge`, `cloudflare-worker`) | -| Envelope shape, `bodyWithEnvelope`, `envelopeAsHeaders` | `@prosopo/protect-edge-core` | -| Block / challenge HTML + branding sanitisers | `@prosopo/protect-edge-core` | -| `passiveCache`, `pathTypeCache` | `@prosopo/protect-edge-core` | -| CloudFront-shaped headers / responses | `lambda-edge` | -| Fetch API `Headers` / `Response` shape, `wrangler.toml` | `cloudflare-worker` | -| Bumblebee routes (`/api/access-check`, `/api/jti/{jti}`, `/api/settings`, `/api/access-rules`) | `crates/bumblebee` | +Pick your platform: -## Next steps +- **[Cloudflare Worker](/protect-edge/cloudflare-worker)** — install, configure, deploy, verify. +- **[AWS Lambda@Edge](/protect-edge/lambda-edge)** — same policy, deployed in front of CloudFront. -- **Cloudflare Workers** — [Deploy the CF Worker](/protect-edge/cloudflare-worker). -- **AWS Lambda@Edge** — same shape, deployed via `serverless deploy` in front of a CloudFront distribution. Environment variables (`BUMBLEBEE_URL`, `CLIENT_JWT`) are inlined at build time via vite because Lambda@Edge rejects functions that carry runtime env vars. +Before you deploy, your Prosopo site must be registered with the Protect service. Site registration is handled from your [Prosopo dashboard](https://portal.prosopo.io/) or by your Prosopo account manager — you will need the site's SS58 key (e.g. `5HSuC1s…`) and a `CLIENT_JWT` signed by the site's key to authenticate the edge to the Protect API. diff --git a/src/content/docs/en/protect-edge/lambda-edge.mdx b/src/content/docs/en/protect-edge/lambda-edge.mdx new file mode 100644 index 0000000000000..53c295bed8f19 --- /dev/null +++ b/src/content/docs/en/protect-edge/lambda-edge.mdx @@ -0,0 +1,144 @@ +--- +title: AWS Lambda@Edge +description: Deploy Prosopo Protect as an AWS Lambda@Edge function in front of a CloudFront distribution so bots and abusive traffic are blocked before they reach your origin. +i18nReady: true +--- + +The Prosopo Protect Lambda@Edge function runs on Amazon CloudFront's viewer-request trigger. It executes Protect's decision tree on every request and either forwards the request to your origin (`allow`), blocks it with a branded interstitial (`block`), or serves a captcha challenge (`challenge`). + +## Prerequisites + +- An AWS account with permission to create Lambda functions in `us-east-1` (required — Lambda@Edge is a `us-east-1` service), publish versions, and attach viewer-request triggers to your CloudFront distributions. +- The [Serverless Framework](https://www.serverless.com/framework/docs/) v4, installed as a dev dependency of the package. +- A Prosopo site key (SS58 address, e.g. `5HSuC1s…`) with Protect enabled on your [Prosopo dashboard](https://portal.prosopo.io/) or by your Prosopo account manager. +- A `CLIENT_JWT` for your site — a long-lived sr25519 JWT signed by your site's key. Your Prosopo dashboard can generate this, or you can mint one locally with `npm run issue-client-jwt`. +- Node 24+ and `npx`. + +## Configuration model + +Lambda@Edge functions **cannot carry environment variables** at runtime — AWS rejects any deploy that sets them. Protect handles this by inlining `BUMBLEBEE_URL` and `CLIENT_JWT` into the bundle at build time via vite's `define`. The bundle is therefore **specific to a site and environment**: rebuild and redeploy to change either value. + +| Value | How it's supplied | Where it lives | +|--|--|--| +| `BUMBLEBEE_URL` (Protect API base URL) | Environment variable at bundle time. | Inlined into `dist/bundle/lambda-edge.js`. | +| `CLIENT_JWT` (secret) | Environment variable at bundle time. | Inlined into `dist/bundle/lambda-edge.js`. | +| Your site key | Baked into your origin HTML — the Protect telemetry bundle reads it as `data-site-key`. | Your origin, not the Lambda. | + +The bundle build fails hard if either value is missing at bundle time, so a Lambda cannot be deployed with an empty Protect URL or JWT. + +## Install & type-check + +```bash +cd protect/packages/lambda-edge +npm install +npm run build:tsc +``` + +## Bundle + +```bash +BUMBLEBEE_URL=https://protect.your-domain.com \ +CLIENT_JWT="eyJhbGciOi..." \ +NODE_ENV=production \ + npm run bundle +``` + +Output lands in `dist/bundle/lambda-edge.js`. + +You can also stash the two values in a `.env` file at the package root — `serverless deploy` (invoked via `npm run deploy` below) auto-loads it: + +``` +BUMBLEBEE_URL=https://protect.your-domain.com +CLIENT_JWT=eyJhbGciOi... +``` + +## Deploy + +`npm run deploy` bundles first, then hands off to `serverless deploy`. It loads `.env` automatically: + +```bash +npm run deploy +``` + +Or explicitly per-invocation without a `.env`: + +```bash +BUMBLEBEE_URL=https://protect.your-domain.com \ +CLIENT_JWT="eyJhbGciOi..." \ + npm run deploy +``` + +Serverless will refuse to deploy if either variable is unset — this catches accidental deploys of a stale `dist/` bundle built against different values. + +Once deployed, attach the function's `viewerRequest` version ARN to your CloudFront distribution's **viewer-request** trigger. Lambda@Edge requires a *published version* ARN (not `$LATEST`); the Serverless Framework emits the correct ARN on successful deploy. + +## npm scripts + +| Script | What it does | +|--|--| +| `clean` | Removes `dist/` and `tsconfig.tsbuildinfo`. | +| `build:tsc` | Type-check. | +| `bundle` | Vite build to `dist/bundle/lambda-edge.js`. Requires `BUMBLEBEE_URL` and `CLIENT_JWT`. | +| `deploy` | Loads `.env`, bundles in production mode, then `serverless deploy`. | +| `typecheck` | Type-only check against `tsconfig.types.json`. | +| `test` | Vitest unit tests. | + +## Verify the deploy + +Once the trigger is attached and the CloudFront invalidation has propagated, test through the distribution's domain (or your CNAME). + +From an unproxied connection: + +```bash +DIST=https://your-distribution.cloudfront.net + +# HTML — should serve your origin (200) +curl -sD - -H "Accept: text/html" -H "User-Agent: Mozilla/5.0" "$DIST/" | head + +# JSON, no cookie — should 401 with no-session status +curl -sD - -H "Accept: application/json" "$DIST/api/anything" | head +# HTTP/2 401 +# x-prosopo-status: no-session +# x-prosopo-request-id: + +# From a proxy or VPN — should 403 with the branded interstitial +curl -sD - -H "Accept: text/html" -x http://your-proxy:port "$DIST/" | head +# HTTP/2 403 +# x-prosopo-decision: block +``` + +`X-Prosopo-Request-Id` is echoed on every response and written to Protect's verdict audit log — grep from a support ticket straight to the verdict. + +CloudFront's own request identifier (`x-amz-cf-id`) is used as the request ID by default, so the same value shows up in CloudFront access logs and in Protect's audit log. + +## Configuring proxy, VPN and datacenter blocking + +Two independent rule sources feed the no-cookie edge path: + +- **Site settings** — recognised IP categories: `tor`, `datacenter`, `proxy`, `vpn`, `abuser`, `mobile`, `crawler`. Configure per-category verdicts (`allow` / `challenge` / `block`) on your Prosopo dashboard. +- **Tiered access rules** — support the full rule set: IP CIDR, ASN, IP category, country, User-Agent substring, 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`). + +## Constraints + +Lambda@Edge has some hard limits worth knowing before you deploy: + +- **Region**: Lambda functions must be created in `us-east-1` even though CloudFront runs them at every edge location. +- **Runtime**: current deploy uses `nodejs24.x`. +- **Memory**: 256 MB. Higher memory tiers cost more per invocation but give proportionally more CPU. +- **Time**: viewer-request timeout is capped at 5 seconds. Protect's calls are budgeted to 500 ms with fail-open on transient errors. +- **No env vars**: as noted above, both `BUMBLEBEE_URL` and `CLIENT_JWT` are inlined at bundle time. +- **Bundle size**: the compressed function code must be under 1 MB (the whole zip under 50 MB). The Protect bundle is well under 100 KB. + +## Certificate handling + +Unlike Cloudflare Workers, Lambda@Edge can be configured to skip TLS verification for outbound HTTPS calls to the Protect API. This is useful in development and staging environments where the Protect API may serve a self-signed certificate. + +## Troubleshooting + +- **`serverless deploy` fails with `A valid variable ... was not found`** — either `BUMBLEBEE_URL` or `CLIENT_JWT` is unset. Set them via `.env` or export them for the deploy. +- **Deploy succeeds but requests return 401 with an internal-error body** — the bundle was built against a stale or wrong `CLIENT_JWT`. Rebundle with the correct value and redeploy. +- **Requests return 502 from CloudFront** — the function threw an unhandled exception. Check the function's CloudWatch logs in the *region that served the request* (Lambda@Edge writes logs to the region closest to the viewer, not `us-east-1`). +- **`Unknown site`** in logs — your site key isn't registered with Protect yet. Check your Prosopo dashboard or contact your account manager. +- **Verdict lookups time out** — the Protect API is unreachable or slow. The function fails open on timeout so requests still reach your origin, but this should be investigated. diff --git a/src/i18n/en/nav.ts b/src/i18n/en/nav.ts index ed391894ebd7d..c2ec9ce1d2bfd 100644 --- a/src/i18n/en/nav.ts +++ b/src/i18n/en/nav.ts @@ -107,6 +107,11 @@ export default [ slug: 'protect-edge/cloudflare-worker', key: 'protect-edge/cloudflare-worker', }, + { + text: 'AWS Lambda@Edge', + slug: 'protect-edge/lambda-edge', + key: 'protect-edge/lambda-edge', + }, {text: 'Framework integrations', header: true, type: 'learn', key: 'framework-integrations'}, { text: 'Angular Integration', From 62280162b3e0d7808101efb14057dcfaa217c081 Mon Sep 17 00:00:00 2001 From: Chris Taylor Date: Tue, 4 Aug 2026 15:01:08 +0100 Subject: [PATCH 06/10] docs(protect-edge): rewrite for bundle-recipient model Both edge integration guides assumed the reader was cloning the source and running the full toolchain. The actual client experience is: they receive a JS bundle, upload it, attach it to the CDN behaviour they want to protect. This rewrite matches that. - Lambda@Edge: 2-step deploy (aws lambda create-function + publish- version, attach the versioned ARN to a CloudFront behaviour). No more references to Serverless Framework as a dev dep, npm scripts, or vite/define internals. - Cloudflare Worker: wrangler.toml template + wrangler deploy. No more `cd protect/packages/cloudflare-worker && npm install`, no bundle/build:tsc script table, no seed-settings walkthrough that needed the admin mnemonic. - Both docs drop internal implementation notes ("inlined at build time via vite's define", "@prosopo/protect-edge-core", etc). Blocking policy is framed as a dashboard task, not an admin API call the client makes. Verified: `npm run check` 0 errors, `npm run build` clean. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../en/protect-edge/cloudflare-worker.mdx | 133 +++++++-------- .../docs/en/protect-edge/lambda-edge.mdx | 158 ++++++++---------- 2 files changed, 132 insertions(+), 159 deletions(-) diff --git a/src/content/docs/en/protect-edge/cloudflare-worker.mdx b/src/content/docs/en/protect-edge/cloudflare-worker.mdx index 4732f31f177c1..f06d19239c35f 100644 --- a/src/content/docs/en/protect-edge/cloudflare-worker.mdx +++ b/src/content/docs/en/protect-edge/cloudflare-worker.mdx @@ -1,88 +1,60 @@ --- title: Cloudflare Worker -description: Deploy Prosopo Protect as a Cloudflare Worker so proxy, VPN, datacenter and Tor traffic is blocked at Cloudflare's edge before it reaches your origin. +description: Deploy the Prosopo Protect Cloudflare Worker in front of your origin so proxy, VPN, datacenter and Tor traffic is blocked at Cloudflare's edge. i18nReady: true --- -The Prosopo Protect Cloudflare Worker sits in front of your origin and runs Protect's decision tree on every incoming request. On `allow` it forwards the request; on `block` / `challenge` it returns immediately without touching your origin. +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` / `challenge` it returns immediately with a branded interstitial (or JSON error for API calls), no origin fetch. + +## What you get + +A single JavaScript file, `worker.js`, delivered per site and per environment. It's self-contained — no dependencies, ~50 KB. ## Prerequisites - A Cloudflare account with **Workers Scripts: Read + Edit** on the API token you'll use. -- A Prosopo site key (SS58 address, e.g. `5HSuC1s…`) with Protect enabled on your [Prosopo dashboard](https://portal.prosopo.io/) or by your Prosopo account manager. -- A `CLIENT_JWT` for your site — a long-lived sr25519 JWT signed by your site's key. Your Prosopo dashboard can generate this, or you can mint one locally with `npm run issue-client-jwt` (see below). -- Node 24+ and `npx`. +- [`wrangler`](https://developers.cloudflare.com/workers/wrangler/install-and-update/) installed locally (`npm i -g wrangler`). +- A Cloudflare zone (for a custom domain) or a workers.dev subdomain (fine for testing). -## Configuration model - -Cloudflare Workers take a different approach from Lambda@Edge: +## Deploy -| Value | How it's supplied | Where it lives | -|--|--|--| -| `BUMBLEBEE_URL` (Protect API base URL) | `wrangler.toml` `[vars]` — public, overridable per deploy with `--var`. | Cloudflare Workers env binding. | -| `CLIENT_JWT` (secret) | `wrangler secret put CLIENT_JWT`. | Cloudflare Workers secret store. | -| Your site key | Baked into your HTML — the Protect telemetry bundle reads it as `data-site-key`. | Your static assets / origin HTML. | +Cloudflare Workers is configured by a small `wrangler.toml` alongside the bundle. Create one next to `worker.js`: -The worker bundle itself is site-agnostic. Client-specific configuration lives in Cloudflare's config store. +```toml +name = "prosopo-protect" +main = "worker.js" +compatibility_date = "2025-01-15" +compatibility_flags = ["nodejs_compat"] -## Install & type-check +# Optional: bind static assets you want the worker to serve when a +# request is allowed. Delete this block if your worker fronts an +# upstream origin instead. +[assets] +directory = "./public" +binding = "ASSETS" +run_worker_first = true -```bash -cd protect/packages/cloudflare-worker -npm install -npm run build:tsc +[vars] +BUMBLEBEE_URL = "https://protect.prosopo.io" ``` -## Deploy +Then deploy: ```bash export CLOUDFLARE_API_TOKEN= -export CLOUDFLARE_ACCOUNT_ID= -# One-off: push the CLIENT_JWT secret (persists across deploys). -npm run deploy:secrets +# 1. Push the authentication token as a wrangler secret (once per environment) +echo "" | wrangler secret put CLIENT_JWT -# Deploy — build:tsc runs first, then wrangler. -npm run deploy +# 2. Deploy +wrangler deploy ``` -To override the Protect API URL for a specific deploy without editing `wrangler.toml`: - -```bash -npm run build:tsc \ - && npx wrangler deploy --var BUMBLEBEE_URL:https://protect.your-domain.com -``` - -To produce a bundle for inspection or hand-off without deploying: - -```bash -npm run bundle # → dist/bundle/index.js + source map -``` - -## npm scripts - -| Script | What it does | -|--|--| -| `clean` | Removes `dist/`, `.wrangler/`, `tsconfig.tsbuildinfo`. | -| `build:tsc` | Type-check via `tsc --build`. | -| `typecheck` | `tsc --build --noEmit`. | -| `bundle` | `wrangler deploy --dry-run --outdir dist/bundle` — bundle to disk, no upload. | -| `deploy` | `build:tsc` then `wrangler deploy`. Loads `.env` if present. | -| `deploy:secrets` | Mints a `CLIENT_JWT` and uploads it as a wrangler secret. | -| `dev` | `wrangler dev` — local dev server. | -| `tail` | `wrangler tail` — live request logs from the deployed worker. | -| `issue-client-jwt` | Prints a fresh 1-year `CLIENT_JWT` signed with `SITE_SURI`. | +The worker is now live at `https://..workers.dev`. To attach to a custom hostname, add a **Route** in `wrangler.toml` (e.g. `route = "example.com/*"`) and redeploy. -`SITE_SURI` is your site's sr25519 secret (mnemonic + optional derivation path) — the same value your site was created with. Export it before running the JWT-issuing script: +When Prosopo issues a new bundle, replace `worker.js` and re-run `wrangler deploy`. The secret persists across deploys. -```bash -SITE_SURI="unusual bulb melt vanish ice correct myth ribbon second tunnel ride sadness" \ - npm run issue-client-jwt -``` - -## Verify the deploy - -From an unproxied connection, HTML requests should pass through so the Protect script can load; API requests without a session should return 401 with `X-Prosopo-Status: no-session`. +## Verify ```bash WORKER=https://your-worker.workers.dev @@ -104,29 +76,48 @@ curl -sD - -H "Accept: text/html" -x http://your-proxy:port "$WORKER/" | head `X-Prosopo-Request-Id` is echoed on every response and written to Protect's verdict audit log — grep from a support ticket straight to the verdict. -## What the worker is protecting +## What the worker protects -Out of the box the worker ships with a small demo page served via wrangler's ASSETS binding. To point it at a real origin, replace the `env.ASSETS.fetch(...)` call at the end of `src/index.ts` with a `fetch(rewrittenUrl, request)` against your upstream and set the origin host in `wrangler.toml`. The Protect decision tree above the fetch is unchanged. +The worker runs Protect logic on every request before serving anything. You can point it at: -The `[assets]` block in `wrangler.toml` sets `run_worker_first = true` so the worker runs before Cloudflare serves any static file — without that, `/` would be served from cache and Protect would never see the request. +- **Static assets** bound via `[assets]` in `wrangler.toml` (as in the example above). Set `run_worker_first = true` so the worker runs before Cloudflare's cache — without this, static files are served directly and Protect never sees the request. +- **An upstream origin** — swap the assets binding for a `fetch(rewrittenUrl, request)` against your upstream, and set the origin host in `wrangler.toml`. The Protect decision above the fetch is unchanged. ## Configuring proxy, VPN and datacenter blocking -Two independent rule sources feed the no-cookie edge path: +Blocking policy lives on your [Prosopo dashboard](https://portal.prosopo.io/), not in the bundle. Two independent rule sources feed the no-cookie edge path: -- **Site settings** — recognised IP categories: `tor`, `datacenter`, `proxy`, `vpn`, `abuser`, `mobile`, `crawler`. Configure per-category verdicts (`allow` / `challenge` / `block`) on your Prosopo dashboard. -- **Tiered access rules** — set via the Protect API. Support the full rule set: IP CIDR, ASN, IP category, country, User-Agent substring, JA4 TLS fingerprint. Rules can be scoped to your site or applied globally by Prosopo. +- **Site settings** — recognised IP categories: `tor`, `datacenter`, `proxy`, `vpn`, `abuser`, `mobile`, `crawler`. Configure per-category verdicts (`allow` / `challenge` / `block`). +- **Tiered access rules** — support the full rule set: IP CIDR, ASN, IP category, country, User-Agent substring, 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`). +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. + +## Rotating credentials + +The authentication token is stored as a wrangler secret, so rotating it is a one-liner and doesn't require a redeploy: + +```bash +echo "" | wrangler secret put CLIENT_JWT +``` + +The next request picks up the new value. ## TLS caveats -Cloudflare Workers doesn't expose a way to skip TLS verification on outbound fetches. If your configured Protect API URL has an invalid or expired certificate, `fetch` returns HTTP 526 and Protect can't be reached. Point `BUMBLEBEE_URL` at a hostname with a valid certificate, or renew the cert on the current host. +Cloudflare Workers doesn't expose a way to skip TLS verification on outbound fetches. If the Protect API URL you're configured with has an invalid or expired certificate, `fetch` returns HTTP 526 and Protect can't be reached. Point `BUMBLEBEE_URL` at a hostname with a valid certificate. + +## Live logs + +While debugging a deploy, `wrangler tail` streams request-by-request logs from the deployed worker: + +```bash +wrangler tail --format=pretty +``` ## Troubleshooting - **`wrangler deploy` returns `Authentication error [code: 10000]`** — the token lacks **Workers Scripts: Read + Edit**. "Edit" alone isn't enough; wrangler needs both. -- **`Unknown site` from `/api/access-check`** — your site key isn't registered with Protect yet. Check your Prosopo dashboard or contact your account manager. -- **Bundle deploys but every request 401s** — `CLIENT_JWT` isn't set or has expired. Re-run `npm run deploy:secrets` and redeploy. +- **Bundle deploys but every request 401s** — the `CLIENT_JWT` secret isn't set or has expired. Re-run `wrangler secret put CLIENT_JWT` and check `wrangler secret list`. - **HTTP 526 in `wrangler tail`** — the Protect API host's certificate is invalid or expired; see [TLS caveats](#tls-caveats). -- **HTML `Access Denied` served instead of JSON on API paths** — the endpoint's declared content type is HTML. Update the site or endpoint's `defaultPathType` to `json` on your Prosopo dashboard. +- **HTML `Access Denied` served instead of JSON on API paths** — the endpoint's declared content type is HTML. Update the site's `defaultPathType` (or add an endpoint rule) on your Prosopo dashboard. +- **`Unknown site` in logs** — your site isn't registered with Protect. Check your dashboard or contact your account manager. diff --git a/src/content/docs/en/protect-edge/lambda-edge.mdx b/src/content/docs/en/protect-edge/lambda-edge.mdx index 53c295bed8f19..72101ca2681a5 100644 --- a/src/content/docs/en/protect-edge/lambda-edge.mdx +++ b/src/content/docs/en/protect-edge/lambda-edge.mdx @@ -1,93 +1,81 @@ --- title: AWS Lambda@Edge -description: Deploy Prosopo Protect as an AWS Lambda@Edge function in front of a CloudFront distribution so bots and abusive traffic are blocked before they reach your origin. +description: Deploy the Prosopo Protect Lambda@Edge bundle in front of a CloudFront distribution so bots and abusive traffic are blocked before they reach your origin. i18nReady: true --- -The Prosopo Protect Lambda@Edge function runs on Amazon CloudFront's viewer-request trigger. It executes Protect's decision tree on every request and either forwards the request to your origin (`allow`), blocks it with a branded interstitial (`block`), or serves a captcha challenge (`challenge`). +The Prosopo Protect Lambda@Edge bundle runs on Amazon CloudFront's viewer-request trigger. It executes Protect's decision on every request and either forwards the request to your origin (`allow`), blocks it with a branded interstitial (`block`), or serves a captcha challenge (`challenge`). -## Prerequisites - -- An AWS account with permission to create Lambda functions in `us-east-1` (required — Lambda@Edge is a `us-east-1` service), publish versions, and attach viewer-request triggers to your CloudFront distributions. -- The [Serverless Framework](https://www.serverless.com/framework/docs/) v4, installed as a dev dependency of the package. -- A Prosopo site key (SS58 address, e.g. `5HSuC1s…`) with Protect enabled on your [Prosopo dashboard](https://portal.prosopo.io/) or by your Prosopo account manager. -- A `CLIENT_JWT` for your site — a long-lived sr25519 JWT signed by your site's key. Your Prosopo dashboard can generate this, or you can mint one locally with `npm run issue-client-jwt`. -- Node 24+ and `npx`. - -## Configuration model - -Lambda@Edge functions **cannot carry environment variables** at runtime — AWS rejects any deploy that sets them. Protect handles this by inlining `BUMBLEBEE_URL` and `CLIENT_JWT` into the bundle at build time via vite's `define`. The bundle is therefore **specific to a site and environment**: rebuild and redeploy to change either value. - -| Value | How it's supplied | Where it lives | -|--|--|--| -| `BUMBLEBEE_URL` (Protect API base URL) | Environment variable at bundle time. | Inlined into `dist/bundle/lambda-edge.js`. | -| `CLIENT_JWT` (secret) | Environment variable at bundle time. | Inlined into `dist/bundle/lambda-edge.js`. | -| Your site key | Baked into your origin HTML — the Protect telemetry bundle reads it as `data-site-key`. | Your origin, not the Lambda. | - -The bundle build fails hard if either value is missing at bundle time, so a Lambda cannot be deployed with an empty Protect URL or JWT. +## What you get -## Install & type-check +A single JavaScript file, `lambda-edge.js`, delivered per site and per environment. It's self-contained — no dependencies, no configuration, ~100 KB. -```bash -cd protect/packages/lambda-edge -npm install -npm run build:tsc -``` - -## Bundle - -```bash -BUMBLEBEE_URL=https://protect.your-domain.com \ -CLIENT_JWT="eyJhbGciOi..." \ -NODE_ENV=production \ - npm run bundle -``` - -Output lands in `dist/bundle/lambda-edge.js`. - -You can also stash the two values in a `.env` file at the package root — `serverless deploy` (invoked via `npm run deploy` below) auto-loads it: +## Prerequisites -``` -BUMBLEBEE_URL=https://protect.your-domain.com -CLIENT_JWT=eyJhbGciOi... -``` +- An AWS account with permission to create Lambda functions in **`us-east-1`** (Lambda@Edge functions must live there), publish versions of that function, and attach viewer-request triggers to your CloudFront distributions. +- A CloudFront distribution serving your origin. +- The [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html) or the AWS Console. ## Deploy -`npm run deploy` bundles first, then hands off to `serverless deploy`. It loads `.env` automatically: +Upload the bundle to Lambda, publish a version, and attach the versioned ARN to the CloudFront behaviour you want to protect. ```bash -npm run deploy +zip lambda-edge.zip lambda-edge.js + +# First deploy: create the function +aws lambda create-function \ + --region us-east-1 \ + --function-name prosopo-protect-viewer-request \ + --runtime nodejs22.x \ + --handler lambda-edge.handler \ + --memory-size 256 \ + --timeout 5 \ + --role arn:aws:iam:::role/ \ + --zip-file fileb://lambda-edge.zip + +# Every deploy after that: update the code +aws lambda update-function-code \ + --region us-east-1 \ + --function-name prosopo-protect-viewer-request \ + --zip-file fileb://lambda-edge.zip + +# Publish a version — Lambda@Edge only accepts versioned ARNs +aws lambda publish-version \ + --region us-east-1 \ + --function-name prosopo-protect-viewer-request ``` -Or explicitly per-invocation without a `.env`: +The `publish-version` response includes a `FunctionArn` ending in `:`. In your CloudFront distribution's **Behaviors** tab, edit the behaviour you want to protect and add a **Function association**: -```bash -BUMBLEBEE_URL=https://protect.your-domain.com \ -CLIENT_JWT="eyJhbGciOi..." \ - npm run deploy -``` - -Serverless will refuse to deploy if either variable is unset — this catches accidental deploys of a stale `dist/` bundle built against different values. +- **Event type**: Viewer request +- **Function type**: Lambda@Edge +- **Function ARN**: the versioned ARN from `publish-version`. -Once deployed, attach the function's `viewerRequest` version ARN to your CloudFront distribution's **viewer-request** trigger. Lambda@Edge requires a *published version* ARN (not `$LATEST`); the Serverless Framework emits the correct ARN on successful deploy. +CloudFront propagates to every edge location in 2–5 minutes. Repeat this attach step for each behaviour that should be protected. When Prosopo issues a new bundle, run `update-function-code` + `publish-version` and point the behaviour at the new versioned ARN — CloudFront won't pick up `$LATEST`. -## npm scripts +### Execution role -| Script | What it does | -|--|--| -| `clean` | Removes `dist/` and `tsconfig.tsbuildinfo`. | -| `build:tsc` | Type-check. | -| `bundle` | Vite build to `dist/bundle/lambda-edge.js`. Requires `BUMBLEBEE_URL` and `CLIENT_JWT`. | -| `deploy` | Loads `.env`, bundles in production mode, then `serverless deploy`. | -| `typecheck` | Type-only check against `tsconfig.types.json`. | -| `test` | Vitest unit tests. | +The function's role needs both `lambda.amazonaws.com` and `edgelambda.amazonaws.com` as trusted principals, plus the `AWSLambdaBasicExecutionRole` managed policy for CloudWatch Logs: -## Verify the deploy +```json +{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": { + "Service": ["lambda.amazonaws.com", "edgelambda.amazonaws.com"] + }, + "Action": "sts:AssumeRole" + } + ] +} +``` -Once the trigger is attached and the CloudFront invalidation has propagated, test through the distribution's domain (or your CNAME). +## Verify -From an unproxied connection: +Once the trigger is attached and CloudFront has propagated, test through the distribution's domain. ```bash DIST=https://your-distribution.cloudfront.net @@ -107,38 +95,32 @@ curl -sD - -H "Accept: text/html" -x http://your-proxy:port "$DIST/" | head # x-prosopo-decision: block ``` -`X-Prosopo-Request-Id` is echoed on every response and written to Protect's verdict audit log — grep from a support ticket straight to the verdict. - -CloudFront's own request identifier (`x-amz-cf-id`) is used as the request ID by default, so the same value shows up in CloudFront access logs and in Protect's audit log. +`X-Prosopo-Request-Id` is echoed on every response and written to Protect's verdict audit log — grep from a support ticket straight to the verdict. CloudFront's own `x-amz-cf-id` is the default request ID, so the same value shows up in CloudFront access logs and Protect's audit log. ## Configuring proxy, VPN and datacenter blocking -Two independent rule sources feed the no-cookie edge path: +Blocking policy lives on your [Prosopo dashboard](https://portal.prosopo.io/), not in the bundle. Two independent rule sources feed the no-cookie edge path: -- **Site settings** — recognised IP categories: `tor`, `datacenter`, `proxy`, `vpn`, `abuser`, `mobile`, `crawler`. Configure per-category verdicts (`allow` / `challenge` / `block`) on your Prosopo dashboard. +- **Site settings** — recognised IP categories: `tor`, `datacenter`, `proxy`, `vpn`, `abuser`, `mobile`, `crawler`. Configure per-category verdicts (`allow` / `challenge` / `block`). - **Tiered access rules** — support the full rule set: IP CIDR, ASN, IP category, country, User-Agent substring, 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`). +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. ## Constraints -Lambda@Edge has some hard limits worth knowing before you deploy: - -- **Region**: Lambda functions must be created in `us-east-1` even though CloudFront runs them at every edge location. -- **Runtime**: current deploy uses `nodejs24.x`. -- **Memory**: 256 MB. Higher memory tiers cost more per invocation but give proportionally more CPU. -- **Time**: viewer-request timeout is capped at 5 seconds. Protect's calls are budgeted to 500 ms with fail-open on transient errors. -- **No env vars**: as noted above, both `BUMBLEBEE_URL` and `CLIENT_JWT` are inlined at bundle time. -- **Bundle size**: the compressed function code must be under 1 MB (the whole zip under 50 MB). The Protect bundle is well under 100 KB. +- **Region**: functions must be created in `us-east-1`. +- **Memory**: 256 MB recommended. Higher tiers cost more but give proportionally more CPU. +- **Time**: the viewer-request handler is capped at 5 seconds. Protect's API calls are budgeted to 500 ms with fail-open on transient errors. +- **CloudWatch logs**: written to the region *closest to the viewer that served the request*, not `us-east-1`. Check the matching region when debugging. -## Certificate handling +## Rotating credentials -Unlike Cloudflare Workers, Lambda@Edge can be configured to skip TLS verification for outbound HTTPS calls to the Protect API. This is useful in development and staging environments where the Protect API may serve a self-signed certificate. +Each bundle is scoped to a specific site's authentication token. Rotating the token means requesting a fresh bundle from Prosopo and redeploying it via the [Deploy](#deploy) flow. The old bundle keeps working until you swap the ARN on your CloudFront behaviour. ## Troubleshooting -- **`serverless deploy` fails with `A valid variable ... was not found`** — either `BUMBLEBEE_URL` or `CLIENT_JWT` is unset. Set them via `.env` or export them for the deploy. -- **Deploy succeeds but requests return 401 with an internal-error body** — the bundle was built against a stale or wrong `CLIENT_JWT`. Rebundle with the correct value and redeploy. -- **Requests return 502 from CloudFront** — the function threw an unhandled exception. Check the function's CloudWatch logs in the *region that served the request* (Lambda@Edge writes logs to the region closest to the viewer, not `us-east-1`). -- **`Unknown site`** in logs — your site key isn't registered with Protect yet. Check your Prosopo dashboard or contact your account manager. -- **Verdict lookups time out** — the Protect API is unreachable or slow. The function fails open on timeout so requests still reach your origin, but this should be investigated. +- **CloudFront returns 502** — the function threw an unhandled exception. Check CloudWatch Logs in the region that served the request (Lambda@Edge writes logs region-locally). +- **Requests return 401 with an internal-error body** — the bundle's authentication token is stale or invalid. Request a fresh bundle from Prosopo and redeploy. +- **CloudFront won't accept the ARN** — Lambda@Edge requires a *published version* ARN (ending in `:1`, `:2`, …). `$LATEST` isn't accepted. +- **Verdict lookups time out in logs** — Protect is unreachable or slow. The function fails open so requests still reach your origin, but investigate. +- **`Unknown site` in logs** — your site isn't registered with Protect. Check your dashboard or contact your account manager. From c2e840e1f285b4b072bef6573f7e848230590f4c Mon Sep 17 00:00:00 2001 From: Chris Taylor Date: Tue, 4 Aug 2026 15:07:00 +0100 Subject: [PATCH 07/10] docs(protect-edge): note account-manager notification for new bundles, gate proxy test on dashboard config MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Both docs: reframe "When Prosopo issues a new bundle" as "your account manager will let you know when a new bundle is available". - Both docs: qualify the proxy/VPN verify curl — it only returns 403 if proxy/VPN blocking is enabled on the Prosopo dashboard. Without the guard, someone running the curl against a fresh site with default rules would see a 200 or 401 instead and think the install was broken. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/content/docs/en/protect-edge/cloudflare-worker.mdx | 6 ++++-- src/content/docs/en/protect-edge/lambda-edge.mdx | 8 ++++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/content/docs/en/protect-edge/cloudflare-worker.mdx b/src/content/docs/en/protect-edge/cloudflare-worker.mdx index f06d19239c35f..54d3115419317 100644 --- a/src/content/docs/en/protect-edge/cloudflare-worker.mdx +++ b/src/content/docs/en/protect-edge/cloudflare-worker.mdx @@ -52,7 +52,7 @@ wrangler deploy The worker is now live at `https://..workers.dev`. To attach to a custom hostname, add a **Route** in `wrangler.toml` (e.g. `route = "example.com/*"`) and redeploy. -When Prosopo issues a new bundle, replace `worker.js` and re-run `wrangler deploy`. The secret persists across deploys. +Your account manager will let you know when a new bundle is available. Replace `worker.js` and re-run `wrangler deploy`. The secret persists across deploys. ## Verify @@ -68,7 +68,9 @@ curl -sD - -H "Accept: application/json" "$WORKER/api/anything" | head # x-prosopo-status: no-session # x-prosopo-request-id: -# From a proxy or VPN — should 403 with the branded interstitial +# From a proxy or VPN (only if you've enabled proxy/VPN blocking on +# your Prosopo dashboard — see the next section) — should 403 with +# the branded interstitial curl -sD - -H "Accept: text/html" -x http://your-proxy:port "$WORKER/" | head # HTTP/2 403 # x-prosopo-decision: block diff --git a/src/content/docs/en/protect-edge/lambda-edge.mdx b/src/content/docs/en/protect-edge/lambda-edge.mdx index 72101ca2681a5..8bdaf4ca526d4 100644 --- a/src/content/docs/en/protect-edge/lambda-edge.mdx +++ b/src/content/docs/en/protect-edge/lambda-edge.mdx @@ -52,7 +52,9 @@ The `publish-version` response includes a `FunctionArn` ending in `: -# From a proxy or VPN — should 403 with the branded interstitial +# From a proxy or VPN (only if you've enabled proxy/VPN blocking on +# your Prosopo dashboard — see the next section) — should 403 with +# the branded interstitial curl -sD - -H "Accept: text/html" -x http://your-proxy:port "$DIST/" | head # HTTP/2 403 # x-prosopo-decision: block From 08d37e265067928f3b88c8fc2e688c100c987722 Mon Sep 17 00:00:00 2001 From: Chris Taylor Date: Tue, 4 Aug 2026 15:08:09 +0100 Subject: [PATCH 08/10] docs(protect-edge): quote typical Protect API latency (<40 ms, <2 ms cached) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The old wording ("budgeted to 500 ms") advertised the failure timeout rather than the expected latency, which reads worse than reality. Replace with the typical numbers first — sub-40 ms live, sub-2 ms on cached lookups — and keep the 500 ms as a hard-timeout fallback. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/content/docs/en/protect-edge/lambda-edge.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/content/docs/en/protect-edge/lambda-edge.mdx b/src/content/docs/en/protect-edge/lambda-edge.mdx index 8bdaf4ca526d4..dd71a6739a7df 100644 --- a/src/content/docs/en/protect-edge/lambda-edge.mdx +++ b/src/content/docs/en/protect-edge/lambda-edge.mdx @@ -114,7 +114,7 @@ Either source firing blocks the request at the edge. When both match, the more r - **Region**: functions must be created in `us-east-1`. - **Memory**: 256 MB recommended. Higher tiers cost more but give proportionally more CPU. -- **Time**: the viewer-request handler is capped at 5 seconds. Protect's API calls are budgeted to 500 ms with fail-open on transient errors. +- **Time**: the viewer-request handler is capped at 5 seconds. Protect's API calls 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. - **CloudWatch logs**: written to the region *closest to the viewer that served the request*, not `us-east-1`. Check the matching region when debugging. ## Rotating credentials From 6901fe1472e2532f41c8b28768949bf06f19471b Mon Sep 17 00:00:00 2001 From: Chris Taylor Date: Tue, 4 Aug 2026 15:09:00 +0100 Subject: [PATCH 09/10] docs(protect-edge): add Performance section to Cloudflare Worker doc MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mirrors the latency numbers now surfaced on the Lambda@Edge doc (sub-40 ms typical, sub-2 ms cached, 500 ms hard timeout with fail-open). Also notes that blocked requests are strictly faster than unprotected ones on the block path — the worker returns immediately without an origin fetch. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/content/docs/en/protect-edge/cloudflare-worker.mdx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/content/docs/en/protect-edge/cloudflare-worker.mdx b/src/content/docs/en/protect-edge/cloudflare-worker.mdx index 54d3115419317..d2c3e79da2d4d 100644 --- a/src/content/docs/en/protect-edge/cloudflare-worker.mdx +++ b/src/content/docs/en/protect-edge/cloudflare-worker.mdx @@ -104,6 +104,10 @@ echo "" | wrangler secret put CLIENT_JWT The next request picks up the new value. +## Performance + +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. + ## TLS caveats Cloudflare Workers doesn't expose a way to skip TLS verification on outbound fetches. If the Protect API URL you're configured with has an invalid or expired certificate, `fetch` returns HTTP 526 and Protect can't be reached. Point `BUMBLEBEE_URL` at a hostname with a valid certificate. From 4c19801e1e56d35b203809a297b229866de42463 Mon Sep 17 00:00:00 2001 From: Chris Taylor Date: Tue, 4 Aug 2026 15:15:45 +0100 Subject: [PATCH 10/10] docs(protect-edge): add DNS/CNAME setup section to both edge guides MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Explain the DNS record clients need to create before deploying: protect. CNAME protect.prosopo.io and the matching CNAME entry on their Prosopo dashboard's site settings. Prosopo uses that hostname to route TLS SNI to the right site config, and TLS cert handling is managed by Prosopo for it. Explicitly rejects NS delegation as an alternative — only CNAME is supported. CF Worker doc: mentions that BUMBLEBEE_URL in wrangler.toml should point at the CNAMEd hostname (not protect.prosopo.io directly). Lambda@Edge doc: notes the bundle is built against the CNAMEd hostname, so credential rotation on the hostname means requesting a fresh bundle. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../en/protect-edge/cloudflare-worker.mdx | 22 +++++++++++++++++++ .../docs/en/protect-edge/lambda-edge.mdx | 22 +++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/src/content/docs/en/protect-edge/cloudflare-worker.mdx b/src/content/docs/en/protect-edge/cloudflare-worker.mdx index d2c3e79da2d4d..7fe65cd89dc62 100644 --- a/src/content/docs/en/protect-edge/cloudflare-worker.mdx +++ b/src/content/docs/en/protect-edge/cloudflare-worker.mdx @@ -15,6 +15,28 @@ A single JavaScript file, `worker.js`, delivered per site and per environment. I - A Cloudflare account with **Workers Scripts: Read + Edit** on the API token you'll use. - [`wrangler`](https://developers.cloudflare.com/workers/wrangler/install-and-update/) installed locally (`npm i -g wrangler`). - A Cloudflare zone (for a custom domain) or a workers.dev subdomain (fine for testing). +- A CNAME record for your Protect API hostname — see the next section. + +## DNS: point your Protect subdomain at Prosopo + +The worker talks to your Protect API over HTTPS at a hostname you own — typically `protect.`. Create a CNAME record on your DNS pointing that hostname at Prosopo: + +``` +protect. CNAME protect.prosopo.io +``` + +Then enter the same hostname (e.g. `protect.example.com`) in the **CNAME** field of your site's Protect settings on your [Prosopo dashboard](https://portal.prosopo.io/). Prosopo uses this to route TLS requests arriving at that SNI to your site's configuration. + +Set `BUMBLEBEE_URL` in `wrangler.toml` (below) to `https://protect.` — the CNAMEd hostname, not `protect.prosopo.io` directly. TLS certificate handling is managed by Prosopo automatically for the CNAMEd hostname. + +Verify the DNS is live before deploying the worker: + +```bash +dig protect. +# should show a CNAME to protect.prosopo.io and an A record. +``` + +**Do not use NS delegation.** Only a CNAME record is supported; NS would delegate the whole subdomain to Prosopo's nameservers, which isn't the model. ## Deploy diff --git a/src/content/docs/en/protect-edge/lambda-edge.mdx b/src/content/docs/en/protect-edge/lambda-edge.mdx index dd71a6739a7df..397a3a0487a15 100644 --- a/src/content/docs/en/protect-edge/lambda-edge.mdx +++ b/src/content/docs/en/protect-edge/lambda-edge.mdx @@ -15,6 +15,28 @@ A single JavaScript file, `lambda-edge.js`, delivered per site and per environme - An AWS account with permission to create Lambda functions in **`us-east-1`** (Lambda@Edge functions must live there), publish versions of that function, and attach viewer-request triggers to your CloudFront distributions. - A CloudFront distribution serving your origin. - The [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html) or the AWS Console. +- A CNAME record for your Protect API hostname — see the next section. + +## DNS: point your Protect subdomain at Prosopo + +The bundle talks to your Protect API over HTTPS at a hostname you own — typically `protect.`. Create a CNAME record on your DNS pointing that hostname at Prosopo: + +``` +protect. CNAME protect.prosopo.io +``` + +Then enter the same hostname (e.g. `protect.example.com`) in the **CNAME** field of your site's Protect settings on your [Prosopo dashboard](https://portal.prosopo.io/). Prosopo uses this to route TLS requests arriving at that SNI to your site's configuration. + +The bundle you receive from Prosopo is built against your CNAMEd hostname — you don't need to configure the URL yourself. If you rotate the CNAMEd hostname, request a fresh bundle. TLS certificate handling is managed by Prosopo automatically for the CNAMEd hostname. + +Verify the DNS is live before deploying the bundle: + +```bash +dig protect. +# should show a CNAME to protect.prosopo.io and an A record. +``` + +**Do not use NS delegation.** Only a CNAME record is supported; NS would delegate the whole subdomain to Prosopo's nameservers, which isn't the model. ## Deploy