@facet-llc/edge-cloudflare
v0.1.0
Published
Cloudflare Worker enforcement layer for Facet. Drop-in agent classification, KYAPay verification, agents.txt discovery, Turnstile fallback, and reputation log emission. Use with the facet-llc/cloudflare-waf-stack Terraform module to ship enforcement to an
Readme
@facet-llc/edge-cloudflare
Drop-in Cloudflare Worker enforcement layer for Facet. Wraps:
- agent traffic classifier (
@facet/classifier) - KYAPay Bearer-issuer observability hook
- agents.txt discovery manifest cache (
@facet/protocol) - Cloudflare Turnstile fallback for unsigned-claiming-human traffic
- reputation flush to a Facet audit endpoint
behind a single factory. Pair with the facet-llc/cloudflare-waf-stack Terraform module to ship enforcement to any site fronted by Cloudflare.
Install
npm install @facet-llc/edge-cloudflare
# or pnpm add @facet-llc/edge-cloudflareRequires Node 20+ at build time. The runtime is the Cloudflare Workers V8 isolate; no Node built-ins.
Usage
import { facetEnforce } from "@facet-llc/edge-cloudflare";
export default {
fetch: facetEnforce({
site_handle: "example",
supplier_email: "[email protected]",
facet_url: "https://audit.facet.llc",
agents_txt_url: "https://example.com/.well-known/agents.txt",
enforce_mode: "shadow", // → "on" after observation
turnstile: {
enabled: true,
site_key_env: "TURNSTILE_SITE_KEY",
secret_env: "TURNSTILE_SECRET_KEY",
challenge_tiers: ["unknown"],
},
reputation: { enabled: true },
suggest: {
terminal_url: "https://api.facet.llc",
issuer_signup_url: "https://issuer.skyfire.xyz/register?ref=facet",
},
}),
};Configuration
| Field | Required | Notes |
| -------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| site_handle | yes | Short tenant identifier sent in reputation flushes (e.g. acme). |
| supplier_email | yes | Email shown in audit reports. |
| facet_url | yes | Base URL of the audit ingest (default Facet operates https://audit.facet.llc). |
| agents_txt_url | no | URL to the site's own /.well-known/agents.txt. When set, the Worker caches the manifest (300s TTL) and surfaces issuer-mismatch via response header for observability. |
| enforce_mode | yes | off (no headers, no decisions), shadow (decide + log + always pass), on (decide + enforce). |
| turnstile.enabled | no | Opt-in challenge fallback. Requires site_key_env + secret_env to be present in the Worker env. Provision via wrangler secret put. |
| turnstile.challenge_tiers | no | Comma-equivalent list of classifier tiers that trigger a challenge (unknown, scraper). Default empty = no challenges. |
| reputation.enabled | no | When true, batches classified lines and flushes aggregates to ${facet_url}/v1/audit/aggregate. Default false. |
| reputation.flush_every_n | no | Lines per flush (default 1000). |
| reputation.flush_every_seconds | no | Max seconds between flushes (default 300). |
| suggest.terminal_url | yes | URL injected into hint/block responses for agent onboarding. |
| suggest.terms_url | no | Defaults to ${terminal_url}/v1/terms. |
| suggest.capabilities_url | no | Defaults to ${terminal_url}/v1/capabilities. |
| suggest.issuer_signup_url | yes | KYA issuer signup URL surfaced in 402 block bodies. |
Decision matrix
| Classifier tier | Bearer present | Action |
| --------------- | -------------- | ------------------------------------------------------------------- |
| human | — | pass |
| unknown | — | pass (or challenge if turnstile + tier in challenge_tiers) |
| agent | no | hint (pass + X-Facet-Suggest header) |
| agent | yes | pass |
| scraper | no | block (HTTP 402) |
| scraper | yes | pass (Terminal validates the token) |
| any | — | pass on infrastructure paths (favicon, robots.txt, /.well-known/*) |
enforce_mode=shadow overrides every block/challenge to a pass-through with a JSON log line, intended for the first 30 minutes of a new deploy so a site operator can spot false positives before flipping to on.
Headers added on pass-through
X-Facet-Action—pass,hint,block, orchallengeX-Facet-Classifier-Tier—human,agent,scraper,unknownX-Facet-Suggest— Terminal URL, on hint actions onlyX-Facet-Issuer-Mismatch—truewhen a Bearer'sissclaim is outside the manifest'sKYA-Issuersallowlist (observability only; the Terminal is the security boundary)
Block response shape
HTTP 402 Payment Required:
{
"error": {
"code": "PAYMENT_REQUIRED",
"message": "Facet classified this request as unauthenticated scraping. Identify via KYAPay + pay per-query at the Terminal to access structured data.",
"retryable": false,
"retry_after_seconds": null,
"suggest": {
"doc": "https://api.facet.llc/v1/terms",
"capabilities": "https://api.facet.llc/v1/capabilities",
"upgrade": "https://api.facet.llc",
"signup": "https://issuer.skyfire.xyz/register?ref=facet"
}
},
"classifier": { "tier": "scraper", "reason": "classifier-scraper" }
}Privacy
The Worker classifies and aggregates in-isolate. No raw log lines are persisted, sent to Facet, or written to disk. Aggregate counts shipped to ${facet_url}/v1/audit/aggregate are bucketed (operator, tier, path category) and contain no IPs, no UAs, and no per-request payloads.
agents.txt is fetched once per cache TTL window per Worker isolate, with If-None-Match revalidation when the upstream supplies an ETag.
Build
pnpm install
pnpm --filter @facet-llc/edge-cloudflare build
pnpm --filter @facet-llc/edge-cloudflare testLicense
Apache-2.0. See LICENSE.
