From 1ec85019af6a0cd0d8b661b73d8136d15117f0ba Mon Sep 17 00:00:00 2001 From: legendko Date: Fri, 30 Oct 2026 12:37:22 +0100 Subject: [PATCH 1/6] Say the console's four steps on the public onboarding page The page promised two parts, while the console opens on naming the business and then sets up a domain in three steps that can wait. The "How do you configure it?" block now names all four and says only the first is required; its two tasks read as the console's last step names them. The register block says the same on its own, because it shows before any preview has drawn the block it used to call "above". The price reads "per fetch" as it does in the console, and the register block no longer says the file and the package carry the publisher's terms: neither does. --- website/src/pages/publishers/onboarding.astro | 30 ++++++++++++------- 1 file changed, 20 insertions(+), 10 deletions(-) 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)); From df034b2a9517f5de40a4608f83b3d2610a785f01 Mon Sep 17 00:00:00 2001 From: legendko Date: Fri, 30 Oct 2026 12:40:43 +0100 Subject: [PATCH 2/6] Say the edge passes a provider's fora.json and rsl.txt through to its origin The edge pages described an edge function that generates, caches and serves both documents, from the Exchange, from KV or from its own settings, and that serves ACME challenge tokens written by a CLI. None of that exists. The edge forwards exactly those two paths to the provider's own origin for every client, ahead of all its checks, and keeps no copy, so the file an agent is sent to, the file an Exchange reads and the file the provider edits are one file. overview.mdx now holds that statement, and the other pages link to it. deployment.mdx loses the token, serving-mode and caching sections, the settings that only fed an edge-built document, and the `fora init` onboarding diagram. The adapter interface and the CloudFront sample stop carrying the documents. The fora.json rate limit is a platform rule in front of the edge, kept above the rate an Exchange reads the file. X-Content-Rules is built from the refused request's host, not from configuration, and the deployment page now says so. --- .../edge-function/bot-detection.mdx | 2 +- .../components/edge-function/cdn-adapters.mdx | 51 +----- .../components/edge-function/deployment.mdx | 165 +++--------------- .../components/edge-function/overview.mdx | 57 +++--- 4 files changed, 65 insertions(+), 210 deletions(-) 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/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. From 8360a5fe2a4beea8aa910873a84688befc226d3f Mon Sep 17 00:00:00 2001 From: legendko Date: Fri, 30 Oct 2026 12:47:56 +0100 Subject: [PATCH 5/6] Stop naming a CLI and an edge KV store in the domain-verification comments The comments on RequestDomainVerification and on the Provider Domain Verification section named a fora-cli tool that does not exist and said an edge function can auto-serve the challenge token from a KV store, which none does. The published reference prints the RPC's comment from the descriptor, so this is the only place to correct it. The contract does not change: comments only, regenerated, with no new rule. The challenge still has the provider place the token at the same address. --- gen/descriptor.binpb | Bin 698253 -> 698163 bytes gen/go/fora/v1/forav1connect/fora.connect.go | 4 ++-- proto/fora/v1/fora.proto | 5 ++--- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/gen/descriptor.binpb b/gen/descriptor.binpb index 87ed02b8253c891a761438f834faa8ec8a34d317..fd07f203829da2fac8b1aa47836cd8d7b5b98b2a 100644 GIT binary patch delta 9409 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�Nx7i``KiG0_E@1Wmeu2$s0&NVb$dU`#Z$ z6>g}5ubR#HfE8{u&KA*8H<60GH9c_-8yKF_>dH$eq*$-j)%|Y@fLO29#l@eZ0R)?^ zu2C&kBZ%`wXbh}k2Yk1%Z%?<+Wp{@g+cR!y8||+A#6MANqus@qW8W>dfu{p+6O7?0 zxh2VQ%lZ=;Sr&MIh_IwL*vDfYi%<7GFJ zC;B>Y0Cf8bi(CQ z3%{P8@-*uoPP`7!v>%d^*hOb|QV--(Yj5U1pU?i3*z77PsYI>4*>zmD%U42cZ{@c< z!`?}3brn&Au%y##E7jmuUkSb2#mj%owkNt=Wtj9tz1!s`Q}k9(%b|ka;_(ISwZvPl zGD<2@^S)Op0zVa{4uP5iJc2OWeYbusAh?%4lA#{$qI0edt4#+#khb>}bmhfjta@CmGs5 z-O#^~#rt@8A&c>U{ule2@lO~3pQRZ%iI_g&DyIa~jeuAe*q=IpNZko{sQ(rRhzy)? zT~AOA2&zxGMW-ayBoI`e45*e81avZ>T8fD3lSK8WbpFN+?K3wtOY_39ZXD&G{Vg(x z&s@tiK4J~i`LJi%cH=WQk`UC#i25_&+NZu?F{lR=Z!!@)#A0XM;t}+s35fW6dBb8> z9q-NP5cR#JbED7!I?JD1%o6dls3?)en4tcwJFZ;nVB_cb5z;y5$_s2vP=C(F1$Mab z1M1K7yd^9dKOdAwOi+KG%Hvj{1M2(ueM{JF@xGuuV&cs0qw?_M0R><1t|eIOi=Y%^ z3Q_+Bm12SL1LnWv5zdCjzYK;_Od;mKq@iRA9SqZpd?IJ5_{E_8W8zkMk=oza!KLyN zpHDiMGJB^xE`vtfzLuHxSw zs`M#OndkABX0ozIVB`n_{~zFhp!q59MllCfK(O_chdD?S2@qxalxNNJ`~#Z=qD-4S zd0ORHx*;CC#y4`s&k1a zt8+LYLId{86A+6m@lbkn*UP8!cJwEIkOJ!#$)Qw`}MDE#POPo69g zlGLMD2s(fAX*8`$7q4XB2*+1>@|1!UTB|%1M7>Z#fj-Aqu3{4s&jtC<7X|v9hr*iW zdqRb-OJ7;VvcidVne}Kot)unC(UMO`OFJ)I&0ZVc?kN?5ZU~{&?p4>|zlDl=v!VUL z3;hYXsuQUi#0Ryq(Z(N$RC;~5nMi%nQ(oftwBq#vzkD_!M^;`8*aXDhUJTd-1e-5< z)=QF25(qXs12&}uv2+G(N)fTyNo;N;HisD6YhGwG0(?;|Naw6!V-({xFLI-pGDC>R z*O5v8jsS?Dfc=vl5bj^6d{A2tAs#pK_I2zwYojM$;2?y~Mh|!PXJ}dvA<8y+%IKlL zDtO|D5AuR&bPz9jo=uNK=)dzpMd-lfeqn)~zro*lo_#Ozh9{e9h#>V1Z)6GkH)Q!1 zLwnN;b@BQ(cF({!y=Y7{%q^7LZJx42HViNbgTVhd0*HXOdHMdB2gE|#yy2#37(k?W xo0r@nQ%nMp;_X3-r3Arl4^k{el;Z73@dMiQ$EQ6pZPea#SzeTX_gfM7{{XjaS~vgz delta 9447 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 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/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 From 507600fa6b5eadc917e85450aed25e4e102b26da Mon Sep 17 00:00:00 2001 From: legendko Date: Fri, 30 Oct 2026 12:50:40 +0100 Subject: [PATCH 6/6] Guard the onboarding docs and record why a publisher's documents are files The public page and the guide follow the console by hand, so a website test now holds both to its step names, the two tasks of its last step, and both step counts, and refuses the names it retired. The doc conformance gate refuses the retired claims: serving modes, the Exchange document endpoints, the CLI and the edge store for challenge tokens. fora-verify stays legal because the contract's messages name it. The design history gains the reasoning behind the edge passing a publisher's documents through, and marks the older section it replaces. Both changelogs carry the change under Unreleased. --- docs/design-history.md | 35 +++++++++++++ proto/CHANGELOG.md | 17 +++++++ scripts/check-doc-conformance.sh | 10 ++++ .../src/content/docs/reference/changelog.mdx | 17 +++++++ .../src/lib/onboarding/console-flow.test.mjs | 50 +++++++++++++++++++ 5 files changed, 129 insertions(+) create mode 100644 website/src/lib/onboarding/console-flow.test.mjs 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/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/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/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}"`); + } + }); +}