diff --git a/docs/design-history.md b/docs/design-history.md index 08a9862d..f3c71eed 100644 --- a/docs/design-history.md +++ b/docs/design-history.md @@ -2958,6 +2958,12 @@ released call still compiles, as the client verbs' call options were added. ## A publisher proves its domain with the manifest it serves +> **Superseded in part — the edge package no longer answers the manifest's address.** The +> manifest is a file on the publisher's origin and the package passes the address through, +> so the authorization below always lives in that file, never in the package's settings. +> The proof, the catalog key and the RPCs stand as written. See "A publisher's documents +> are files on its origin". + The contract carries `RequestDomainVerification` and `ConfirmDomainVerification`, an ACME-style challenge in which the publisher places a token and the Exchange fetches it. The publisher guide does not walk through them, because the console publishers onboard @@ -3266,3 +3272,32 @@ visible rather than silent. The chain cases the Go tests replay are kept outside `*-vectors.json` convention until those ports exist, because that convention is a promise of tri-language replay and the completeness gate enforces it without exemption. + +## A publisher's documents are files on its origin + +An edge package used to carry a publisher's manifest and RSL file in its settings and +answer both addresses itself. That made two copies of the document that decides who may +sell a publisher's pages: the file the console handed over and the values in the package. +A publisher who edited the file changed nothing an agent or the Exchange read, and on +CloudFront which copy answered depended on which cache behavior the function was attached +to. A publisher's own `/rsl.txt` was hidden the same way, behind a 404 from the package. + +So the edge serves no publisher document. GET and HEAD of exactly `/.well-known/fora.json` +and `/rsl.txt` go to the origin for every client, AI bots included, ahead of the bot check, +the licensed-path check and signed URL verification, and the origin's answer comes back +unchanged. A bot refused at an article is told to read that document, so it has to be able +to, whatever it calls itself. The match is exact, so the rule frees nothing else. + +The reasons are the ones a single file gives. There is one copy, under the publisher's +control. It behaves the same on every CDN and cache-behavior layout. A publisher adds an +Exchange or changes a contributor by editing the file, with nothing to redeploy, and nothing +drifts when another network is added. The cost is that a publisher has to place the file, +which the console's last step asks for, and that an unreachable origin is a `502`: a +document the edge made up would say nothing about the domain. + +Two things left the docs with it. `/provider/{domain}/fora.json` and +`/provider/{domain}/rsl.txt` were Exchange endpoints for an edge to fetch, and an edge was +their only reader. And no `fora-cli` ever drove the domain-verification challenge, and no +edge store ever answered it. The RPC pair itself stays in the contract, for deployments +that verify that way, and a publisher answering such a challenge places the token on its +origin like any other file. diff --git a/gen/descriptor.binpb b/gen/descriptor.binpb index 87ed02b8..fd07f203 100644 Binary files a/gen/descriptor.binpb and b/gen/descriptor.binpb differ diff --git a/gen/go/fora/v1/forav1connect/fora.connect.go b/gen/go/fora/v1/forav1connect/fora.connect.go index 4e9ae056..af66bc95 100644 --- a/gen/go/fora/v1/forav1connect/fora.connect.go +++ b/gen/go/fora/v1/forav1connect/fora.connect.go @@ -145,7 +145,7 @@ type ExchangeServiceClient interface { // the filing failed. DisputeTransaction(context.Context, *connect.Request[v1.DisputeRequest]) (*connect.Response[v1.DisputeResponse], error) // Request a domain verification challenge for provider onboarding. - // Used by fora-cli to prove domain control before pushing signing keys. + // Proves domain control before signing keys are pushed. // Follows the ACME HTTP-01 pattern (Let's Encrypt). RequestDomainVerification(context.Context, *connect.Request[v1.DomainVerificationRequest]) (*connect.Response[v1.DomainVerificationChallenge], error) // Confirm domain verification and register a signing key. @@ -328,7 +328,7 @@ type ExchangeServiceHandler interface { // the filing failed. DisputeTransaction(context.Context, *connect.Request[v1.DisputeRequest]) (*connect.Response[v1.DisputeResponse], error) // Request a domain verification challenge for provider onboarding. - // Used by fora-cli to prove domain control before pushing signing keys. + // Proves domain control before signing keys are pushed. // Follows the ACME HTTP-01 pattern (Let's Encrypt). RequestDomainVerification(context.Context, *connect.Request[v1.DomainVerificationRequest]) (*connect.Response[v1.DomainVerificationChallenge], error) // Confirm domain verification and register a signing key. diff --git a/proto/CHANGELOG.md b/proto/CHANGELOG.md index e001b1ca..31d8429d 100644 --- a/proto/CHANGELOG.md +++ b/proto/CHANGELOG.md @@ -1,5 +1,22 @@ # FORA Protocol Changelog +## Unreleased + +**The publisher docs describe the console's onboarding and the file on the publisher's +origin (docs and comments only; no wire change).** The public onboarding page and the +publisher guide walk the console's four steps: Your business, which is required, then +Your domain, Your terms and Connect your site, which can be finished later and which +every further domain takes again. Prices read "per fetch", and those two pages call the +artifact the discovery file. The edge pages say a provider's `fora.json` and `rsl.txt` +are files on its own origin: the edge forwards exactly those two paths for every client, +ahead of all its checks, and keeps no copy, so the serving modes, the caching strategy +and the settings that fed an edge-built document are gone. A domain is proven by the +`fora.json` it serves. `RequestDomainVerification` and `ConfirmDomainVerification` stay in +the contract, and their comments no longer name a `fora-cli` tool or an edge KV store. The +Exchange pages lose the `/provider/{domain}/fora.json` and `/provider/{domain}/rsl.txt` +endpoints, whose only reader was such an edge. The documentation conformance gate refuses +the retired phrasings, and a website test holds both pages to the console's step names. + ## v1.2.0 ### Breaking against v1.1.0 diff --git a/proto/fora/v1/fora.proto b/proto/fora/v1/fora.proto index d489b06e..8463b913 100644 --- a/proto/fora/v1/fora.proto +++ b/proto/fora/v1/fora.proto @@ -536,7 +536,7 @@ service ExchangeService { rpc DisputeTransaction(DisputeRequest) returns (DisputeResponse); // Request a domain verification challenge for provider onboarding. - // Used by fora-cli to prove domain control before pushing signing keys. + // Proves domain control before signing keys are pushed. // Follows the ACME HTTP-01 pattern (Let's Encrypt). rpc RequestDomainVerification(DomainVerificationRequest) returns (DomainVerificationChallenge); @@ -4751,13 +4751,12 @@ message DisputeResponse { // // ACME HTTP-01 style domain verification for provider onboarding. // Proves that the caller controls the provider domain before accepting -// signing keys or configuration changes. Used by fora-cli. +// signing keys or configuration changes. // // Flow: // 1. Provider calls RequestDomainVerification with their domain // 2. Exchange returns a challenge token // 3. Provider places the token at {domain}/.well-known/fora-verify/{token} -// (edge function can auto-serve this from KV store) // 4. Provider calls ConfirmDomainVerification // 5. Exchange fetches the challenge URL to verify domain control // 6. If verified: Exchange accepts subsequent operations for this domain diff --git a/scripts/check-doc-conformance.sh b/scripts/check-doc-conformance.sh index c1bbeaf5..d1fce692 100755 --- a/scripts/check-doc-conformance.sh +++ b/scripts/check-doc-conformance.sh @@ -134,6 +134,16 @@ patterns=( 'estimateTolerance' 'EstimateToleranceBps' 'MeteredSettlement' 'metered_settlement' 'meteredSettlement' 'SettleMeteredUsage' 'settle_metered_usage' 'settleMeteredUsage' 'settling-a-metered-purchase' + # The edge serves no publisher document: a provider's fora.json and rsl.txt are + # files on its origin, and the edge forwards both addresses for every client. So + # no serving mode picks a source for them, no Exchange endpoint hands them to an + # edge, and no edge store answers the domain-verification challenge. These are the + # retired phrasings of those claims, with the CLI that was said to drive them. + # fora-verify is NOT listed: the contract's domain-verification messages name that + # path, and the RPC pair stays. The /provider/ pattern names the two documents + # because /provider/{domain}/transactions is a live audit route. + 'fora-cli' 'auto-serve' 'serving_mode' 'servingMode' 'exchange_api_url' 'exchangeApiUrl' + '/provider/\{domain\}/(fora\.json|rsl\.txt)' ) # Files where naming a removed identifier is legitimate (they record history). diff --git a/website/src/content/docs/architecture/deployment-models.mdx b/website/src/content/docs/architecture/deployment-models.mdx index e61125e8..d90987c5 100644 --- a/website/src/content/docs/architecture/deployment-models.mdx +++ b/website/src/content/docs/architecture/deployment-models.mdx @@ -7,17 +7,9 @@ Three deployment models cover the full provider spectrum: enterprise providers w ## Provider Onboarding: Domain Verification (v1.0) -Before any deployment model, the provider must prove it controls its domain. With the contract's domain-verification RPCs, that is an ACME HTTP-01 style challenge, handled by `fora-cli`: +Before any deployment model, the provider must prove it controls its domain. The proof is the `fora.json` the domain serves from the provider's own origin. The Exchange reads that file at registration, on every check of a domain being verified and at every catalog push, so the proof keeps being renewed: a file that stops listing the Exchange stops the domain's pushes. [Publisher Onboarding](/getting-started/publisher-onboarding/) describes that path through a publisher console. -1. Provider runs `fora-cli verify --domain techcrunch.com --exchange exchange.ssp-alpha.com` -2. CLI calls `RequestDomainVerification` -- Exchange returns a challenge token -3. CLI places the token at `{domain}/.well-known/fora-verify/{token}` (the edge function can auto-serve this from KV store) -4. CLI calls `ConfirmDomainVerification` -- Exchange fetches the challenge URL to verify domain control -5. If verified: Exchange accepts subsequent operations (signing key registration, catalog pushes) for this domain - -The Exchange also checks `fora.json` to verify it is listed as an authorized exchange for this provider -- providing double protection. - -An Exchange can instead take the `fora.json` the domain serves as that proof, as the Exchange behind a publisher console does; [Publisher Onboarding](/getting-started/publisher-onboarding/) describes that path. +The contract also carries `RequestDomainVerification` and `ConfirmDomainVerification`, for deployments that verify a domain with a challenge instead (see the [contract reference](/reference/proto-fora/#messages--domain-verification)). ### catalog_contributors in fora.json @@ -43,22 +35,24 @@ The Exchange operator deploys and maintains the FORA edge function on the provid ### How It Works 1. Provider signs a contract with the Exchange operator and grants CDN access (e.g., AWS IAM role for CloudFront, Cloudflare API token for Workers) -2. Exchange operator runs domain verification on behalf of the provider (`fora-cli verify`) -3. Exchange operator deploys the edge function to the provider's CDN -4. Exchange operator configures: `fora.json` (including `catalog_contributors`), `rsl.txt` generation from the Exchange, bot detection rules -5. Exchange operator maintains, updates, and monitors the edge function on an ongoing basis +2. Exchange operator generates the provider's `fora.json` (including `catalog_contributors`) and, where the provider has none, its `rsl.txt` +3. Provider publishes those files on its own origin, which proves it controls the domain +4. Exchange operator deploys the edge function to the provider's CDN +5. Exchange operator configures bot detection rules +6. Exchange operator maintains, updates, and monitors the edge function on an ongoing basis ### Provider Actions - Sign contract with Exchange operator - Grant CDN role/access to the Exchange operator -- Nothing else -- the Exchange operator handles all FORA infrastructure +- Publish the generated files on its own origin +- The Exchange operator handles the rest of the FORA infrastructure ### Exchange Operator Responsibilities - Deploy edge function to provider's CDN account -- Generate and serve `fora.json` from Exchange tenant config -- Generate and serve `rsl.txt` from Exchange catalog +- Generate `fora.json` from Exchange tenant config, for the provider to publish on its origin +- Generate `rsl.txt` from Exchange catalog, where the provider has none of its own - Configure bot detection rules (User-Agent matching, behavioral signals) - Rotate signing keys on schedule (default: 90 days) - Monitor edge function health, latency, and error rates @@ -81,7 +75,7 @@ The provider deploys the edge function themselves using tooling provided by FORA ```bash npx @fora/edge-deploy --cdn cloudfront --exchange exchange.ssp.com ``` -3. Edge function pulls `fora.json` and `rsl.txt` from the Exchange, caches in KV/edge storage +3. Provider publishes `fora.json`, and `rsl.txt` if it has one, on its own origin; the edge function passes both through to it 4. Provider manages their own CDN access and can customize bot detection rules ### Supported CDNs @@ -149,15 +143,15 @@ For CDNs with limited compute capabilities, the edge function can be reduced to 1. **Signed URL verification**: verify the Ed25519 signature over the canonical URL -- valid? pass through : return 403. A CloudFront distribution verifies its RSA signed URLs natively, so there the function skips this step. 2. **Bot detection**: Check `User-Agent` for known AI crawler strings -- match? return 403 with `X-Content-Rules` header -3. **Serve `fora.json`**: Return `/.well-known/fora.json` inline or proxied from the Exchange +3. **Let `fora.json` through**: Leave `/.well-known/fora.json` to the origin, ahead of the bot check, so a refused bot can read it ### Minimum Viable Decision Table ``` Request arrives at edge: -1. Path = /.well-known/fora.json? - YES -> Return inline fora.json (or proxy from Exchange) +1. Path = /.well-known/fora.json or /rsl.txt? + YES -> Pass through to origin 2. Has signed URL params (sig, exp)? YES -> Verify signature @@ -170,4 +164,4 @@ Request arrives at edge: 4. Default -> Pass through to origin (non-bot, non-protected) ``` -Step 1 needs a runtime that can run code: Cloudflare Workers and Fastly Compute verify the signature themselves, and a CloudFront distribution verifies its own RSA signed URLs without any function at all. A declarative rule language -- Fastly VCL, Akamai Property Manager -- cannot verify a signature, so it can serve `fora.json` and refuse known crawlers but cannot admit a paid request. +Step 1 needs a runtime that can run code: Cloudflare Workers and Fastly Compute verify the signature themselves, and a CloudFront distribution verifies its own RSA signed URLs without any function at all. A declarative rule language -- Fastly VCL, Akamai Property Manager -- cannot verify a signature, so it can refuse known crawlers but cannot admit a paid request. diff --git a/website/src/content/docs/architecture/production-architecture.mdx b/website/src/content/docs/architecture/production-architecture.mdx index 4a28cfb0..a2a5cc92 100644 --- a/website/src/content/docs/architecture/production-architecture.mdx +++ b/website/src/content/docs/architecture/production-architecture.mdx @@ -91,7 +91,7 @@ Dispute lifecycle: `FILED` -> `AUTO_RESOLVED` / `EVIDENCE_NEEDED` -> `UNDER_REVI ### Provider Domain Verification (v1.0) -The `RequestDomainVerification` and `ConfirmDomainVerification` RPCs provide ACME HTTP-01 style domain verification for provider onboarding. Providers prove domain control before the Exchange accepts signing keys or configuration changes. The WBA directory at `/.well-known/http-message-signatures-directory` enables verification vendors to publish their Ed25519 keys (in its `keys`) for attestation validation. FORA has no dedicated verifier role; a verifier publishes under its operator's role (`ROLE_PUBLISHER` for a self-attesting publisher, `ROLE_EXCHANGE` for a standalone vendor). +A provider proves it controls its domain with the `fora.json` it serves, which the Exchange reads before it accepts catalog pushes or configuration changes for that domain, and again at each push. The contract also carries the `RequestDomainVerification` and `ConfirmDomainVerification` RPCs, for deployments that verify a domain with a challenge instead. The WBA directory at `/.well-known/http-message-signatures-directory` enables verification vendors to publish their Ed25519 keys (in its `keys`) for attestation validation. FORA has no dedicated verifier role; a verifier publishes under its operator's role (`ROLE_PUBLISHER` for a self-attesting publisher, `ROLE_EXCHANGE` for a standalone vendor). ### Discovery: Three Complementary Standards diff --git a/website/src/content/docs/components/content-ingestion/rsl-generation.mdx b/website/src/content/docs/components/content-ingestion/rsl-generation.mdx index d37d4662..70b8a086 100644 --- a/website/src/content/docs/components/content-ingestion/rsl-generation.mdx +++ b/website/src/content/docs/components/content-ingestion/rsl-generation.mdx @@ -5,12 +5,12 @@ description: "How the Exchange generates standards-compliant rsl.txt from the co ## Exchange as RSL Producer -The Exchange is both an RSL consumer (ingesting provider rsl.txt to build the catalog) and an RSL producer (generating rsl.txt for providers who don't have one). This enables a zero-configuration provider onboarding path: the Exchange operator sets tenant defaults, and the Exchange generates a standards-compliant rsl.txt that the edge function can serve. +The Exchange is both an RSL consumer (ingesting provider rsl.txt to build the catalog) and an RSL producer (generating rsl.txt for providers who don't have one). This enables a zero-configuration provider onboarding path: the Exchange operator sets tenant defaults, and the Exchange generates a standards-compliant rsl.txt that the provider publishes on its own origin. ```mermaid sequenceDiagram - participant PUB as Provider (no rsl.txt) participant MP as Exchange + participant PUB as Provider's Origin (no rsl.txt yet) participant EDGE as Edge Function participant AGENT as AI Agent @@ -19,11 +19,13 @@ sequenceDiagram MP->>MP: Build catalog from tenant defaults + sitemap MP->>MP: Generate rsl.txt from catalog - AGENT->>PUB: GET /rsl.txt - PUB->>EDGE: Viewer request - EDGE->>MP: GET /provider/{domain}/rsl.txt - MP-->>EDGE: Generated RSL XML - EDGE-->>AGENT: rsl.txt (cached at edge) + MP-->>PUB: Generated RSL XML, handed over by the operator + Note over PUB: Provider publishes it at /rsl.txt + + AGENT->>EDGE: GET /rsl.txt + EDGE->>PUB: Forward the request + PUB-->>EDGE: rsl.txt + EDGE-->>AGENT: The origin's answer, unchanged ``` ## RSL Generator @@ -137,13 +139,7 @@ For a tenant with defaults "flat, $0.05, permits ai-input + search, prohibits ai ## Serving the Generated RSL -The Exchange exposes the generated RSL via an HTTP endpoint: - -``` -GET /provider/{domain}/rsl.txt -``` - -The provider's edge function fetches and caches this, serving it at the standard `/rsl.txt` path. Cache TTL matches the ingestion refresh interval (default: 24h for full refresh, 5m for incremental). +The provider publishes the generated document at the standard `/rsl.txt` path on its own origin, and replaces it when the catalog changes. The edge function [passes that address through](/components/edge-function/overview/#forajson-and-rsltxt-pass-through) for every client, so an agent reads the file the provider published. ## FORA Sitemap Namespace (XSD) diff --git a/website/src/content/docs/components/edge-function/bot-detection.mdx b/website/src/content/docs/components/edge-function/bot-detection.mdx index 75b7480b..a06a3444 100644 --- a/website/src/content/docs/components/edge-function/bot-detection.mdx +++ b/website/src/content/docs/components/edge-function/bot-detection.mdx @@ -147,7 +147,7 @@ Target: **100 requests/minute per IP** before throttling. **Level 2: Per-IP rate limiting on fora.json** -fora.json is cacheable, but an aggressive agent might poll it. Rate limit to **10 requests/minute per IP** and rely on `Cache-Control` for normal operation. +fora.json is cacheable, but an aggressive agent might poll it. Rate limit to **10 requests/minute per IP** and rely on `Cache-Control` for normal operation. This is a platform rule in front of the edge function, which only forwards the path to the origin. An Exchange reads the file at registration, on every check of a domain being verified and at every catalog push, so keep the limit above that rate. Rate limiting implementation: - **Cloudflare**: Use `request.cf.botManagement` and Cloudflare Rate Limiting rules (not in Worker code -- use platform rate limiting for efficiency). diff --git a/website/src/content/docs/components/edge-function/cdn-adapters.mdx b/website/src/content/docs/components/edge-function/cdn-adapters.mdx index 6323770a..cb3b1d4a 100644 --- a/website/src/content/docs/components/edge-function/cdn-adapters.mdx +++ b/website/src/content/docs/components/edge-function/cdn-adapters.mdx @@ -53,12 +53,6 @@ interface EdgeKV { } interface EdgeConfig { - /** WellKnownManifest for fora.json generation. */ - manifest: WellKnownManifest; - - /** RSL text content for /rsl.txt serving. */ - rslTxt: string; - /** Glob patterns that require a license (carry FORA terms); all other paths * are open. Compiled from RSL/catalog entries — a local edge type, not a wire * message. */ @@ -72,12 +66,6 @@ interface EdgeConfig { * an Exchange the manifest lists (e.g. "exchange.example"), never a URL. */ exchangeDomain?: string; - /** Exchange API base URL for fetching fora.json and rsl.txt. */ - exchangeApiUrl?: string; - - /** Serving mode for fora.json and rsl.txt. */ - servingMode: "exchange" | "inline" | "kv" | "origin"; - /** Bot User-Agent patterns to detect (regexes). */ botPatterns: RegExp[]; @@ -117,7 +105,7 @@ interface EdgeConfig { ```mermaid graph LR subgraph "CloudFront Distribution" - CF_FUNC["CloudFront Function
(viewer request)
Bot detection + fora.json + rsl.txt
Signed URL passthrough"] + CF_FUNC["CloudFront Function
(viewer request)
Bot detection
fora.json + rsl.txt passthrough
Signed URL passthrough"] BEHAVIOR["Cache Behavior
trusted_key_groups
for /premium/*"] end @@ -131,7 +119,7 @@ graph LR LAMBDA --> DDB ``` -- **CloudFront Function** handles bot detection, fora.json serving, and signature passthrough. CloudFront's `trusted_key_groups` on the cache behavior natively verifies RSA-signed URLs -- no custom code needed. +- **CloudFront Function** handles bot detection and signature passthrough, and hands `/.well-known/fora.json` and `/rsl.txt` back to CloudFront, which fetches them from the origin. CloudFront's `trusted_key_groups` on the cache behavior natively verifies RSA-signed URLs -- no custom code needed. - **Lambda@Edge** (optional) handles agent identity binding and single-use enforcement. These require network access (DynamoDB lookup) which CloudFront Functions cannot do. **What CloudFront does natively that you do not reimplement**: @@ -141,7 +129,7 @@ graph LR **What you must implement in the function**: - Bot detection (User-Agent check against `botPatterns`). -- fora.json and rsl.txt serving. +- Handing `/.well-known/fora.json` and `/rsl.txt` to the origin, ahead of bot detection. - Custom parameters validation (agent_id binding). - Single-use enforcement (requires Lambda@Edge + DynamoDB, best-effort). - The 403 response body and the `X-Content-Rules` and `X-FORA-Exchange` headers. @@ -153,30 +141,9 @@ function handler(event) { var uri = request.uri; var qs = request.querystring; - // Serve fora.json - if (uri === '/.well-known/fora.json') { - return { - statusCode: 200, - statusDescription: 'OK', - headers: { - 'content-type': { value: 'application/json' }, - 'cache-control': { value: 'public, max-age=3600' } - }, - body: MANIFEST_JSON - }; - } - - // Serve rsl.txt - if (uri === '/rsl.txt') { - return { - statusCode: 200, - statusDescription: 'OK', - headers: { - 'content-type': { value: 'text/plain' }, - 'cache-control': { value: 'public, max-age=3600' } - }, - body: RSL_TXT - }; + // The provider's own files: the origin answers these, for every client + if (uri === '/.well-known/fora.json' || uri === '/rsl.txt') { + return request; // pass through } // Only process protected paths @@ -235,7 +202,7 @@ function handler(event) { ```mermaid graph LR subgraph "Cloudflare Edge" - WORKER["Cloudflare Worker
Bot detection
fora.json + rsl.txt serving
Ed25519 signature verification
Agent identity binding"] + WORKER["Cloudflare Worker
Bot detection
fora.json + rsl.txt passthrough
Ed25519 signature verification
Agent identity binding"] KV["Workers KV
Config cache
Bot pattern updates"] DO["Durable Object
(optional)
Single-use enforcement
Atomic check-and-consume"] end @@ -284,7 +251,7 @@ mean writing the Ed25519 verifier as an EdgeWorker, which the platform's 4 ms bu ```mermaid graph LR subgraph "Fastly Edge" - COMPUTE["Compute Service (WASM)
Bot detection
fora.json + rsl.txt serving
Ed25519 signature verification
Agent identity binding
Single-use enforcement"] + COMPUTE["Compute Service (WASM)
Bot detection
fora.json + rsl.txt passthrough
Ed25519 signature verification
Agent identity binding
Single-use enforcement"] KV_STORE["KV Store
Config + single-use tracking
(strongly consistent in-POP)"] CONFIG_STORE["Config Store
Signing keys, bot patterns"] end @@ -298,7 +265,7 @@ graph LR | Feature | CloudFront Function | CloudFront + Lambda@Edge | Cloudflare Worker | Fastly Compute | |---|---|---|---|---| | Bot detection | Yes | Yes | Yes | Yes | -| fora.json + rsl.txt serving | Yes | Yes | Yes | Yes | +| fora.json + rsl.txt passthrough | Yes | Yes | Yes | Yes | | Native signed URL verification | Yes (trusted_key_groups) | Yes (trusted_key_groups) | No (must implement) | No (must implement) | | Ed25519 signed-URL verification | No (1ms limit, no Ed25519) | Yes | Yes (native Web Crypto) | Yes (injected verifier) | | Agent identity binding | No (no network) | Yes | Yes | Yes | diff --git a/website/src/content/docs/components/edge-function/deployment.mdx b/website/src/content/docs/components/edge-function/deployment.mdx index a3bae53e..6e4ceef9 100644 --- a/website/src/content/docs/components/edge-function/deployment.mdx +++ b/website/src/content/docs/components/edge-function/deployment.mdx @@ -11,34 +11,37 @@ The Edge Function is the only FORA component that providers deploy. This page co sequenceDiagram participant Provider as Provider participant MO as Exchange Operator + participant Origin as Provider's Origin participant CDN as Provider's CDN - participant FORA as FORA CLI Provider->>MO: Sign resource access agreement - MO-->>Provider: Exchange endpoint + credentials + MO-->>Provider: Exchange endpoint + fora.json - Provider->>FORA: fora init --cdn cloudfront --exchange exchange.ssp.com - Note over FORA: Generates edge function config
Outputs CDN-specific deployment artifact - - FORA-->>Provider: EdgeConfigDocument + deployment instructions + Provider->>Origin: Publish /.well-known/fora.json + Note over Origin: The provider's own file
rsl.txt, if any, lives here too Provider->>CDN: Deploy edge function (platform-specific) Note over CDN: CloudFront: deploy CF Function + update behavior
Cloudflare: wrangler deploy
Fastly: fastly compute deploy Provider->>CDN: Verify: GET /.well-known/fora.json - CDN-->>Provider: 200 OK + WellKnownManifest + CDN->>Origin: Forward the request + Origin-->>CDN: 200 OK + WellKnownManifest + CDN-->>Provider: The origin's answer, unchanged Provider->>CDN: Verify: curl -A "GPTBot" /premium/article.html CDN-->>Provider: 403 + X-Content-Rules (+ X-FORA-Exchange) ``` +Where the Exchange operator runs a publisher console, the console generates the file and the edge package for the provider: see [Publisher Onboarding](/getting-started/publisher-onboarding/). + ## Simplest Possible Onboarding The absolute minimum a provider needs to do: 1. **Get an Exchange endpoint** from their Exchange operator (one-time business relationship). -2. **Deploy the edge function** using the Terraform module or manual deployment (see below). -3. **Verify** -- `curl -I https://techcrunch.com/.well-known/fora.json` returns the manifest. +2. **Publish `fora.json`** on their own origin, at `/.well-known/fora.json`. +3. **Deploy the edge function** using the Terraform module or manual deployment (see below). +4. **Verify** -- `curl -I https://techcrunch.com/.well-known/fora.json` returns the origin's file through the edge function. import { Aside } from '@astrojs/starlight/components'; @@ -54,7 +57,7 @@ The edge function is distributed in multiple formats to match provider deploymen ### Format 1: CloudFront Function (Reference Implementation) -The reference implementation is a vanilla JavaScript CloudFront Function that handles bot detection, signed URL verification, and `fora.json` serving. +The reference implementation is a vanilla JavaScript CloudFront Function that handles bot detection and signed URL verification. -**Key provisioning uses ACME domain verification.** You prove you control your domain (just like getting a TLS certificate from Let's Encrypt), and the Exchange provisions your Ed25519 signing key: - -``` -1. fora-cli key push -> Exchange issues ACME challenge -2. Place challenge at /.well-known/fora-verify/{token} -3. Exchange fetches -> domain ownership proven -4. Key is provisioned -> ready to sign attestations -``` - **Key publication**: Your domain hosts a WBA directory — a JWK Set at `/.well-known/http-message-signatures-directory` — carrying your public keys (inline RFC 7517 Ed25519 JWKs, each identified by its RFC 7638 thumbprint). Agents and Exchanges fetch this to verify your attestation signatures; keys no longer live in `fora.json`. FORA has no dedicated verifier role; a verifier publishes under its operator's role — here `ROLE_PUBLISHER` for self-attestation. **`catalog_contributors`**: You can authorize third-party verification vendors to push attestations on your behalf. Declare authorized contributors in your provider manifest. You may always push for your own domain; besides your own domain, only contributors listed in `catalog_contributors` can push resource metadata to the Exchange for it. @@ -195,7 +186,7 @@ Your provider manifest can declare `supported_profiles` to signal which profiles ## The `fora.json` Provider Manifest -Every provider with a web presence can host a `fora.json` file at their domain root (like `ads.txt` for advertising). This file declares: +Every provider with a web presence can host a `fora.json` file at `/.well-known/fora.json` on their domain (like `ads.txt` for advertising). This file declares: - Which Exchange(s) are authorized to sell your resources - Which extension profiles your resources support @@ -203,7 +194,7 @@ Every provider with a web presence can host a `fora.json` file at their domain r Your public verification keys are published separately in your WBA directory (the JWK Set at `/.well-known/http-message-signatures-directory`), not in `fora.json`. -AI agents and Brokers read `fora.json` to discover how to access your resources through FORA. +AI agents and Brokers read `fora.json` to discover how to access your resources through FORA. You serve the file from your own origin and the edge function [passes it through](/components/edge-function/overview/#forajson-and-rsltxt-pass-through), so you change any of this by editing the file. ## FAQ @@ -227,7 +218,7 @@ FORA's core protocol works without profiles. You get discovery, pricing, transac ## Next Steps -1. **Web-facing content providers**: Where your Exchange operator runs a publisher console, register your site there; [Publisher Onboarding](/getting-started/publisher-onboarding/) walks you through it. Otherwise, ask your Exchange operator if they support FORA. If yes, they deploy everything. If no, [self-service deployment](/architecture/deployment-models/) takes 15 minutes. +1. **Web-facing content providers**: Where your Exchange operator runs a publisher console, register your site there; [Publisher Onboarding](/getting-started/publisher-onboarding/) walks you through it. Otherwise, ask your Exchange operator if they support FORA. If yes, they deploy the edge function and you publish the file they generate. If no, [self-service deployment](/architecture/deployment-models/) takes 15 minutes. 2. **Data and API providers**: Contact a [Exchange operator](/getting-started/for-exchange-operators/) to discuss integration. 3. **All providers**: Review [How Money Flows](/getting-started/how-money-flows/) to understand the billing chain and pricing models. diff --git a/website/src/content/docs/getting-started/industry-landscape.mdx b/website/src/content/docs/getting-started/industry-landscape.mdx index 2794cdcf..4b465028 100644 --- a/website/src/content/docs/getting-started/industry-landscape.mdx +++ b/website/src/content/docs/getting-started/industry-landscape.mdx @@ -39,7 +39,7 @@ The open transaction protocol that completes the stack. - Adds discovery (`fora.json`), transaction execution, signed URL delivery, and usage reporting - Resource attestation (three verification levels) and automated dispute resolution -- ACME-style provider onboarding with domain verification and CDN key provisioning +- Provider onboarding where the `fora.json` a domain serves proves control of that domain - Layers on RSL (ingests `rsl.txt` for pricing) and CoMP (wraps CoMP objects in transaction messages) - Multi-exchange architecture with an optional Broker for cross-exchange comparison diff --git a/website/src/content/docs/getting-started/publisher-onboarding.mdx b/website/src/content/docs/getting-started/publisher-onboarding.mdx index 2ce8c90f..d9a8e361 100644 --- a/website/src/content/docs/getting-started/publisher-onboarding.mdx +++ b/website/src/content/docs/getting-started/publisher-onboarding.mdx @@ -1,20 +1,20 @@ --- title: "Publisher Onboarding" -description: "Register your site on the publisher console: publish one file, install one package on your CDN and set your terms. The operator imports your catalog from your sitemap." +description: "Register your site on the publisher console: name your business, add your domain, set your terms, then serve one file on your site and install one package on your CDN. The operator imports your catalog from your sitemap." --- import { Aside, LinkButton } from '@astrojs/starlight/components'; import { FORA_PUBLISHER_CONSOLE_URL } from 'astro:env/client'; -The publisher console takes your site from registration to its first sale. You tell it your domain and your terms, and it generates the two things that run on your side: a discovery document and an edge package for your CDN. You place one file and one function. From then on, AI agents pay for your pages and your readers are unaffected. +The publisher console takes your site from registration to its first sale. You name your business and tell it your domain and your terms, and it generates the two things that run on your side: a discovery file (`fora.json`) and an edge package for your CDN. You serve one file and install one package. From then on, AI agents pay for your pages and your readers are unaffected. You generate no keys and you never push a catalog. The operator, the company that runs the console and the Exchange, imports your pages from your sitemap. Registering costs nothing. Start at the publisher console. ## Before you start - Your domain is served by Cloudflare or AWS CloudFront, the two networks the console sets up. For any other network, see [If your network is not supported](#if-your-network-is-not-supported). -- You can publish a file at `/.well-known/fora.json` on that domain. -- You can deploy to your CDN account: a Worker on Cloudflare, or a Lambda@Edge function on CloudFront. +- You can put a file at `/.well-known/fora.json` on that domain, where anyone can read it, AI agents included. +- You can deploy to your CDN account: a Worker on Cloudflare, which needs Node.js 22 or newer on your machine, or a Lambda@Edge function on CloudFront. - Your site has a sitemap, at `/sitemap.xml` or named on a `Sitemap:` line in `robots.txt`. The console finds it by itself. Without one, your pages are not imported automatically. ## Create your account @@ -25,53 +25,106 @@ The console sends you to its sign-in page first, and that page also lets you cre An account made with an email address needs confirming. Open the message you receive, follow the link, then sign in again. Until you do, the console shows only "Please confirm your email address". A Google account arrives already confirmed. -## Add your domain +Your first sign-in opens the setup on its first step, "Your business". -Once you are signed in, the console offers "Add your first domain". Press "Add domain" to start. The setup has five steps: "Your business", "Your domain", "Publish a file", "Install the package" and "Set your terms". +## The four steps - +The first time through, the setup has four steps. Only the first is required before you can use the console. The other three set up a domain, and you can finish them later. + +| Step | What you do | Required? | +|---|---|---| +| Your business | Name the business your payments are made out to | Yes | +| Your domain | Say which domain you license | No, you can finish it later | +| Your terms | Choose what agents may do with your pages and what they pay | No, you can finish it later | +| Connect your site | Serve one file on your site and install one package on your CDN | No, you can finish it later | + +You name your business once. Every domain you add afterwards takes the last three steps (see [Add another domain](#add-another-domain)). + +Each step's "Next" saves what the step asked, so leaving loses nothing you have saved (see [Finish later](#finish-later)). ### Your business -The console asks for your business name once, before your first domain. It is the name your payments are made out to, and you cannot change it in the console later. +The console asks "What is your business called?" This is the name your payments are made out to. You give it once, and you cannot change it in the console later. + +This step has no "Finish later". Until your business is named, every page of the console brings you back here. Once you press "Next", the setup moves on and does not return to this step. ### Your domain -Enter the hostname only, such as `www.example.com`, with no `https://` in front and no path after it, then press "Check". The console works out which CDN serves the domain, because that decides how the rest of the setup goes. +Enter the hostname only, such as `www.example.com`, with no `https://` in front and no path after it. The console works out which CDN serves the domain, because that decides how the rest of the setup goes. It looks the domain up when you press Enter, or when you press "Next" if you have not. Under "Licensed paths", list the parts of the domain you license, separated by commas, like `/articles/, /archive/`. Leave the field empty to license the whole domain. -If the domain is already registered to another publisher, the console says so. If it is yours, ask the operator to transfer it. +"Next" saves the domain and its paths, and neither can be changed afterwards. From then on the hostname is held for you, and the domain is listed in "My domains" as "Not verified". Nothing on it is sold until it is verified. + +The console saves nothing, and says why, in these cases: + +- The domain is registered to another publisher. If it is yours, ask the operator to transfer it. +- The domain is already one of yours. The console links to it instead. +- You already have 10 domains that are not verified. Finish setting one of them up before you add another. +- The console cannot set up the network that serves the domain (see [If your network is not supported](#if-your-network-is-not-supported)). + +### Your terms + +Choose what an AI agent may do with your pages and what it pays. The console shows the offer an agent would get as you choose. + +- Price: "Free" or "Charge". Charge fills in a default price per fetch, in the Exchange's currency, and you can change it. +- Uses: "Search" (listing and linking your pages), "AI input" (answers and retrieval, quoting your pages) and "AI training" (training a model on your pages). +- Attribution: "Required" means every use must credit you. + +If your site publishes an RSL licensing file, the form opens on the terms it states. Otherwise it opens on free, with search and AI input allowed and AI training not. + +Pages of your domain that were in the catalog before automatic import, for example from a feed, keep their own terms unless you tick "Replace the terms of pages that are already in the catalog". Their titles and descriptions are kept either way. + +"Next" saves your terms on the domain. You can change them until the domain is verified. After that the domain's page shows them, and the console does not let you change them yet. -### Publish a file +### Connect your site -The console generates your `fora.json`. Download it and serve it, unchanged, at `https:///.well-known/fora.json`. The console checks for it as soon as you open this step, and "Next" unlocks once the file is found and matches. +This step has two tasks, "Serve your discovery file" and "Install the package on your CDN". Both are on screen from the start, and you can do them in either order. When both are in place, your domain is verified. -The file proves that the domain is yours. It also names the Exchange that sells access to your pages, the account that gets paid, and the operator's catalog contributor, which imports your pages (see [Keys and your catalog](#keys-and-your-catalog)). The [fora.json example](/reference/fora-json-example/) describes the standard fields. The account that gets paid is an entry of the operator's own, in the `ext` of the Exchange's entry, which the example does not cover. +#### Serve your discovery file -The check reads the file from your domain. It does not test delivery through your CDN; the next step checks part of that. If the file does not match yet, the console says why. The fix is always the same: serve the downloaded file exactly as it is, then press "Check again". +Download the file and serve it, unchanged, at `https:///.well-known/fora.json`, from your own site. -### Install the package +The file tells AI agents and the Exchange who sells access to your pages and who gets paid, and serving it proves the domain is yours. It names the Exchange, the account that gets paid, and the operator's catalog contributor, which imports your pages (see [Keys and your catalog](#keys-and-your-catalog)). The file is public and holds no secret. The [fora.json example](/reference/fora-json-example/) describes the standard fields. The account that gets paid is an entry of the operator's own, in the `ext` of the Exchange's entry, which the example does not cover. -The package is the one function you place. It runs in front of your licensed paths: it lets people and search crawlers through, refuses AI agents that identify themselves and have no license, and accepts an agent that comes back with the signed address the Exchange gave it. The console builds it for your domain, with your settings already in it. It holds no secret; it fetches the Exchange's public keys. +The Exchange reads the file from your domain, and the task says what it found: + +- "We found no file at /.well-known/fora.json on your site yet." +- "We read the file at your site and it does not match yet." The console lists each thing that differs. The fix is always the same: serve the downloaded file exactly as it is. +- "Your file is in place and names your account." + +Once the package answers, the console also asks for the file as an AI agent, because the package sends every agent it refuses to this address. If your site refuses them there, the task says so, and you have to let them read it. + +#### Install the package on your CDN + +The package runs in front of your licensed paths: it lets people and search crawlers through, refuses AI agents that identify themselves and have no license, and accepts an agent that comes back with the signed address the Exchange gave it. The console builds it for your domain, with your settings already in it. It holds no secret; it fetches the Exchange's public keys. + +The package serves no document of yours. It [passes](/components/edge-function/overview/#forajson-and-rsltxt-pass-through) `/.well-known/fora.json` and `/rsl.txt`, those exact addresses, through to your site for every visitor, AI agents included, so the file you serve is the one agents read. The package recognizes AI agents mainly by the name they give, so a scraper that passes itself off as a browser or a search crawler gets through. Keep your CDN's bot protection for those (see [Bot Detection](/components/edge-function/bot-detection/#bot-classification-tiers)), but do not let it refuse AI agents that announce themselves: that refuses paying agents too, and the console's package check cannot pass. -The console shows the commands for your network. +The console shows the commands for your network under "Show the deploy commands". -#### Cloudflare +##### Cloudflare -Download `fora-cloudflare.zip`. It holds the Worker and its `wrangler.toml`, with your settings and your route already filled in. Unpack it and deploy it with Wrangler. +Download `fora-cloudflare.zip`. It holds the Worker and its `wrangler.toml`, with your settings and your route already filled in. Unpack it and deploy it with Wrangler, which needs Node.js 22 or newer: -The route covers your whole hostname, because the Worker also serves your discovery document at `/.well-known/fora.json`, built from the settings in the package. From then on, that copy is the one your domain serves. +```sh +unzip fora-cloudflare.zip -d fora-edge +cd fora-edge +npx wrangler login +npx wrangler deploy +``` + +The login opens your browser once to authorize your Cloudflare account. The deploy uploads the Worker and attaches it to your route. + +The route covers your whole hostname, so the Worker sees every path you license. Which paths it guards is a setting in the package, not the route. -#### AWS CloudFront +##### AWS CloudFront Download `fora-lambda-edge.zip`. It holds the function, with your settings beside it, because Lambda@Edge takes no environment variables. Then: @@ -79,31 +132,34 @@ Download `fora-lambda-edge.zip`. It holds the function, with your settings besid 2. Publish a version of it. CloudFront attaches a published version, never `$LATEST`. 3. In your distribution, edit the cache behavior for your licensed paths and add that version as a viewer-request association. -The function also answers `/.well-known/fora.json` from its settings, but only for requests that reach it. If the cache behavior you attached it to also handles that path, as the default behavior does, its copy is the one your domain serves; otherwise your origin keeps serving your file. - -#### Check the package +#### When both are in place -Once the package is deployed, the console checks your live site, asking both as an AI agent and as an ordinary browser. It confirms three things: an AI agent that identifies itself and has no license is refused, people and search crawlers are let through, and your discovery document is still served. You can go on once the package is confirmed and no check has failed. These checks do not complete a paid delivery. +The console checks both tasks when the step opens, again every 15 seconds while the page is open, and at once when you press "Check now". It pauses while the tab is hidden. -If your discovery document stops answering after you deploy, the package's routes have to cover the well-known paths as well. +For the package, it asks your site both as an AI agent and as an ordinary browser. An AI agent without a license has to be refused and sent to buy access, and people and search crawlers have to be let through. If something other than the package refuses the request, such as a bot filter, the task says so. These checks do not complete a paid delivery. -### Set your terms +When the file and the package both pass, the console asks the Exchange to verify the domain, and the step says the domain is verified. "Go to your domain" opens its page, which shows your terms and "Your setup files", which you can download again. -Choose what an AI agent may do with your pages and what it pays. This is what an agent is offered when it asks for one of them. +## Finish later -- Price: "Free" or "Charge". Charge fills in a default price per unit, in the Exchange's currency, and you can change it. -- Uses: "Search" (listing and linking your pages), "AI input" (answers and retrieval, quoting your pages) and "AI training" (training a model on your pages). -- Attribution: "Required" means every use must credit you. +Every step after "Your business" has a "Finish later" link until the domain is verified. It takes you to "My domains". Closing the tab has the same effect, because each "Next" has already saved what its step asked. -If your site publishes an RSL licensing file, the form opens on the terms it states. Otherwise it opens on free, with search and AI input allowed and AI training not. +- Before "Next" on "Your domain", nothing about the domain is saved. +- After it, the domain stays in "My domains" as "Not verified", with everything you entered kept, and the hostname stays held for you. Nothing on it is sold until both tasks of "Connect your site" pass. -Pages of your domain that were in the catalog before automatic import, for example from a feed, keep their own terms unless you tick "Replace the terms of pages that are already in the catalog". Their titles and descriptions are kept either way. +To pick up where you left off, press "Finish setup" on the domain, in "My domains" or on the domain's page. The setup reopens on "Your terms" if you have not chosen any, and on "Connect your site" otherwise. + +You cannot remove a saved domain from the console. If you saved one you no longer want, ask the operator to release it. -Press "Finish setup". The console reads your discovery document once more and saves the domain. The domain's page then shows your terms and "Your setup files", which you can download again. The console does not let you change the terms yet. +## Add another domain + +In "My domains", press "Add domain". Your business is already named, so the setup has three steps, numbered from 1: "Your domain", "Your terms" and "Connect your site". They work as described above, each domain has its own terms and its own files, and each one can be finished later. + +Before you have saved a domain, "My domains" says "No domains yet." and offers the same button. ## After setup @@ -118,17 +174,21 @@ The rest of the console shows what your pages earn: ## Keys and your catalog -You generate no keys and upload none, and you never push your catalog yourself. The operator holds the catalog contributor key that imports your pages, and your discovery document authorizes the operator's contributor to do that for your domain. Serve it as the console generated it: removing that authorization stops the import, and nothing on the Exchange side can restore it. Where the edge package answers `/.well-known/fora.json` (see [Install the package](#install-the-package)), the document comes from the package's settings, so editing your file changes nothing there: a change goes into those settings, and the package is deployed again. The edge package holds no secret either; it fetches the Exchange's public keys. +You generate no keys and upload none, and you never push your catalog yourself. The operator holds the catalog contributor key that imports your pages, and your discovery file authorizes the operator's contributor to do that for your domain. The edge package holds no secret either; it fetches the Exchange's public keys. + +The file on your site is the document. The package keeps no copy of it, and the Exchange reads it from your site again for each import, through a short-lived cache. Editing that file is how you change who may sell or import your pages, and nothing needs redeploying. + +Keep the Exchange's entry and the account in it: a file that stops naming them stops the domain's imports. Removing the operator's contributor stops them too, and nothing on the Exchange side can restore that authorization. ## If your network is not supported -The console sets up sites served by Cloudflare or AWS CloudFront. For any other network, or when it cannot tell which network serves your domain, it stops at the domain step and shows an address to write to. A site behind a service that hides its origin can end up here too, so write even if you do use one of the two. +The console sets up sites served by Cloudflare or AWS CloudFront. For any other network, or when it cannot tell which network serves your domain, it stops at "Your domain" with nothing saved and shows an address to write to. A site behind a service that hides its origin can end up here too, so write even if you do use one of the two. ## Without a publisher console A publisher console is the operator's tool, not part of the protocol. Where an operator runs FORA without one, a publisher sets up the same pieces directly: -- The discovery document: the [fora.json example](/reference/fora-json-example/) and the field reference in [Exchange Manifest](/protocol/exchange-manifest/). +- The discovery file: the [fora.json example](/reference/fora-json-example/) and the field reference in [Exchange Manifest](/protocol/exchange-manifest/). - The edge function: [Edge function deployment](/components/edge-function/deployment/). - The catalog, pushed with the publisher's own key or with a contributor key that the publisher's `fora.json` authorizes: [Pushing with the Publisher SDK](/components/content-ingestion/sources/#pushing-with-the-publisher-sdk) and the [JSONL Ingestion Feed](/protocol/jsonl-ingestion/). - Self-attestation with the publisher's own signing key: [Content Attestation](/protocol/content-attestation/). diff --git a/website/src/content/docs/index.mdx b/website/src/content/docs/index.mdx index 5804b557..367e54dd 100644 --- a/website/src/content/docs/index.mdx +++ b/website/src/content/docs/index.mdx @@ -35,7 +35,7 @@ import { Card, CardGrid, LinkCard } from '@astrojs/starlight/components'; Automated dispute handling for delivery failures, content hash mismatches, and token count discrepancies. 100% of routine disputes resolved without human review. - Where an operator runs a publisher console, register a site there, publish one generated file and install one edge package on your CDN. No keys to generate, no catalog to push. + Where an operator runs a publisher console, name your business, add your domain and connect your site: one generated file on your site and one package on your CDN. No keys to generate, no catalog to push. No walled gardens. Exchange operators, providers, and third parties operate Exchanges. A Broker compares offers across them. diff --git a/website/src/content/docs/reference/changelog.mdx b/website/src/content/docs/reference/changelog.mdx index 70766baf..00255214 100644 --- a/website/src/content/docs/reference/changelog.mdx +++ b/website/src/content/docs/reference/changelog.mdx @@ -6,6 +6,23 @@ description: "FORA protocol changelog" This page contains curated public release notes. For the exhaustive maintainer and protocol history, see [`proto/CHANGELOG.md`](https://github.com/FORA-Protocol/protocol/blob/main/proto/CHANGELOG.md). +## Unreleased + +**The publisher docs describe the console's onboarding and the file on the publisher's +origin (docs and comments only; no wire change).** The public onboarding page and the +publisher guide walk the console's four steps: Your business, which is required, then +Your domain, Your terms and Connect your site, which can be finished later and which +every further domain takes again. Prices read "per fetch", and those two pages call the +artifact the discovery file. The edge pages say a provider's `fora.json` and `rsl.txt` +are files on its own origin: the edge forwards exactly those two paths for every client, +ahead of all its checks, and keeps no copy, so the serving modes, the caching strategy +and the settings that fed an edge-built document are gone. A domain is proven by the +`fora.json` it serves. `RequestDomainVerification` and `ConfirmDomainVerification` stay in +the contract, and their comments no longer name a `fora-cli` tool or an edge KV store. The +Exchange pages lose the `/provider/{domain}/fora.json` and `/provider/{domain}/rsl.txt` +endpoints, whose only reader was such an edge. The documentation conformance gate refuses +the retired phrasings, and a website test holds both pages to the console's step names. + ## v1.2.0 ### Breaking against v1.1.0 diff --git a/website/src/lib/onboarding/console-flow.test.mjs b/website/src/lib/onboarding/console-flow.test.mjs new file mode 100644 index 00000000..c6e8967f --- /dev/null +++ b/website/src/lib/onboarding/console-flow.test.mjs @@ -0,0 +1,50 @@ +// The public onboarding page and the publisher guide describe the publisher +// console, so they name its steps the way it does. The authority is the list the +// reference implementation's console draws in its wizard header; both files here +// follow it by hand, and this keeps them from drifting apart or back. They are +// read as text because neither can be imported here: one is a page, the other MDX. + +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { readFileSync } from 'node:fs'; + +const read = (path) => readFileSync(new URL(path, import.meta.url), 'utf8'); + +const SURFACES = { + 'public onboarding page': read('../../pages/publishers/onboarding.astro'), + 'publisher guide': read('../../content/docs/getting-started/publisher-onboarding.mdx'), +}; + +const STEPS = ['Your business', 'Your domain', 'Your terms', 'Connect your site']; +const CONNECT_TASKS = ['Serve your discovery file', 'Install the package on your CDN']; + +// Screens the console no longer has, and the words these two files replaced. +const RETIRED = [ + 'Publish a file', + 'Add your first domain', + 'Skip for now', + 'Who gets paid?', + 'Step 1 ·', + 'Step 2 ·', + 'discovery document', + 'Per access', +]; + +for (const [surface, text] of Object.entries(SURFACES)) { + test(`the ${surface} names the console's steps and the last step's two tasks`, () => { + for (const name of [...STEPS, ...CONNECT_TASKS]) { + assert.ok(text.includes(name), `the ${surface} does not name "${name}"`); + } + }); + + test(`the ${surface} tells the first run's four steps from a later domain's three`, () => { + assert.match(text, /four steps/i); + assert.match(text, /three steps/i); + }); + + test(`the ${surface} uses no name the console has retired`, () => { + for (const name of RETIRED) { + assert.ok(!text.includes(name), `the ${surface} still says "${name}"`); + } + }); +} diff --git a/website/src/pages/publishers/onboarding.astro b/website/src/pages/publishers/onboarding.astro index 65b765be..81fa51a7 100644 --- a/website/src/pages/publishers/onboarding.astro +++ b/website/src/pages/publishers/onboarding.astro @@ -186,6 +186,10 @@ import '../../styles/brand.css'; .configuration .cmd { max-width: none; width: 100%; } .configuration .card h3 { margin-top: 0; } .embed { margin-top: 1.25rem; } + /* brand.css resets every element's padding, which takes a list's numbers with it. */ + .configuration ol { color: var(--gray); max-width: 46rem; margin-top: 1rem; padding-left: 1.5rem; } + .configuration li + li { margin-top: .35rem; } + .configuration li strong { color: var(--light-gray); font-weight: 600; } /* sample */ .sample { margin-block: 1.25rem; } @@ -393,12 +397,12 @@ import '../../styles/brand.css'; Pricing model
- +
- Price per access -
EUR
+ Price per fetch +
EUR

@@ -422,12 +426,18 @@ import '../../styles/brand.css';

    How do you configure it?

    -

    The configuration has two parts: a discovery file that identifies your Exchange, and a CDN integration that checks access to licensed content.

    -

    Step 1 · Publish the discovery file we generate for you

    -

    We’ll generate your fora.json with the Exchange details already filled in. You publish it at /.well-known/fora.json on your domain so agents and Exchanges can find it. Below is a preview for the domain you provided.

    +

    The publisher console takes you through four steps. The first, your business name, is required. The other three set up a domain: you can finish them later, and each domain you add after that takes the same three steps.

    +
      +
    1. Your business. The name your payments are made out to.
    2. +
    3. Your domain. The domain you license, and which of its paths.
    4. +
    5. Your terms. What an agent may do with your pages and what it pays, as in the controls above.
    6. +
    7. Connect your site. One file on your site and one package on your CDN, in any order. When both are in place, your domain is verified.
    8. +
    +

    Serve your discovery file

    +

    We’ll generate your fora.json with the Exchange details already filled in. You serve it from your own site at /.well-known/fora.json so agents and Exchanges can find it. Below is a preview for the domain you provided.

    -

    Step 2 · Connect your CDN

    -

    Your CDN checks signed delivery URLs before granting access to licensed content. The integration works in front of your existing site. The detected network determines the integration path below.

    +

    Install the package on your CDN

    +

    Your CDN checks signed delivery URLs before granting access to licensed content. The package works in front of your existing site. The detected network determines the integration path below.

    @@ -438,7 +448,7 @@ import '../../styles/brand.css';
    Ready to connect your site?

    Register on the publisher console.

    -

    The console walks you through both steps above. It generates your discovery file and your CDN package with your site and your terms already filled in. You publish one file and connect one function. Registering is free.

    +

    The console takes you through four steps: Your business, Your domain, Your terms and Connect your site. Only your business name is required to get started; you can finish the other three later. The console generates your discovery file and your CDN package with your site’s details already filled in, so you serve one file on your site and install one package on your CDN. Registering is free.

    Register on the publisher console (opens in a new tab) Read the publisher guide (opens in a new tab) @@ -601,7 +611,7 @@ import '../../styles/brand.css'; const manifest = state.preview?.manifest; if (!manifest) { $('manifest').replaceChildren( - 'We could not generate your discovery document for this domain. ', contactLink(CONTACT_LABEL), ' and we will send it.'); + 'We could not generate your discovery file for this domain. ', contactLink(CONTACT_LABEL), ' and we will send it.'); } else { $('manifest').innerHTML = `# https://${esc(d)}/.well-known/fora.json (configuration preview)\n` + highlightJson(JSON.stringify(manifest, null, 2));