diff --git a/README.md b/README.md index 7281f99..f0d9296 100644 --- a/README.md +++ b/README.md @@ -8,8 +8,8 @@
-**[letsauth.eu](https://letsauth.eu/)** is an open, vendor-neutral lab for seeing, testing and breaking how AI agents are authorized. -MCP, A2A and agent delegation meet OAuth, OpenID Connect and workload identity, with a European lens. +**[letsauth.eu](https://letsauth.eu/)** is an open, vendor-neutral lab for how AI agents are authorized. +It covers where MCP, A2A and agent delegation meet OAuth, OpenID Connect and workload identity, with EU law in view.
@@ -40,25 +40,23 @@ MCP, A2A and agent delegation meet OAuth, OpenID Connect and workload identity, ## Why this lab exists Vendors explain agent authorization through their own product. Conformance testers run against one -server, by its owner, pinned to an older revision of the spec. Nobody shows, over time and for -everyone, which public MCP servers validate the issuer, bind the audience, or still lean on dynamic -client registration. Nobody draws WIMSE, AAuth, ID-JAG, A2A and the EU acts on one map. - -letsauth measures instead of asserting. It keeps one open dataset of what the agent protocols -require and what real servers do: - -- **Every claim cites a revision.** Each requirement links to the section of the specification - revision it comes from. When the spec moves, the atlas says what changed and keeps the old - reading intact. -- **Every status cites an observation.** A cell is only ever a dated observation with a method and - evidence a reviewer can open. Nothing is inferred from silence: an empty cell reads *unknown*, - never failure. -- **Open, append-only data.** The dataset is YAML in this repository under CC BY 4.0. Observations - are never edited, so the atlas is also a record of how the ecosystem moved. -- **Neutral and passive.** Rows are alphabetical, nothing is ranked, and the site contacts no server - at build time. The scanner reads public metadata only, never with a token. -- **European by default.** Hosting, storage and analytics run in the EU. eIDAS 2.0, the AI Act and - NIS2 are first-class content wherever they touch agent delegation. +server, by its owner, pinned to an older revision of the spec. Nobody shows which public MCP +servers validate the issuer, bind the audience or still rely on dynamic client registration, for +all of them and over time. Nobody draws WIMSE, AAuth, ID-JAG, A2A and the EU acts on one map. + +letsauth keeps one open dataset of what the agent protocols require and what real servers do: + +- **Each requirement cites a revision.** It links to the section of the specification revision it + comes from. When the spec changes, the atlas says what changed and keeps the old reading. +- **Each status cites an observation.** A cell is a dated observation with a method and evidence a + reviewer can open. A cell with no observation on record reads *unknown*, and never reads as a + failure. +- **Open, append-only data.** The dataset is YAML in this repository under CC BY 4.0. We never edit + an observation, so the atlas also records how servers changed over time. +- **Neutral and passive.** Rows are in alphabetical order and nothing is ranked. The site build + contacts no server. The scanner reads public metadata only and never sends a token. +- **European.** Hosting, storage and analytics run in the EU. The site covers eIDAS 2.0, the AI + Act and NIS2 wherever they touch agent delegation. ## What is live @@ -82,14 +80,14 @@ the data. One shared model, many views. Specifications, features, servers, observations and the relations -between standards live as YAML under [`data/`](data/), validated by a Zod schema at build time and -changed only through reviewed pull requests. The site is pre-rendered from that data; the scanner -is the one backend, and its output flows back into the repository as data. The full picture is in +between standards live as YAML under [`data/`](data/). A Zod schema validates them at build time, +and they change only through reviewed pull requests. The build pre-renders the site from that +data. The scanner is the one backend, and its output comes back into the repository as data. The full picture is in [`docs/architecture.md`](docs/architecture.md). ## Use the data -Everything the site shows is yours to download under CC BY 4.0: +You can download everything the site shows, under CC BY 4.0: ```sh curl -s https://letsauth.eu/mcp/data.json | jq '.servers | length' @@ -120,5 +118,5 @@ issue. ## Licences Code is [MIT](LICENSE). Data under `data/` and the JSON exports are -[CC BY 4.0](LICENSE-DATA). Operated by [Jan Brennenstuhl](https://www.janbrennenstuhl.eu/) in -Berlin as Let's Auth Labs; see the [imprint](https://letsauth.eu/imprint/). +[CC BY 4.0](LICENSE-DATA). [Jan Brennenstuhl](https://www.janbrennenstuhl.eu/) runs the site in +Berlin as Let's Auth Labs. The [imprint](https://letsauth.eu/imprint/) has the details. diff --git a/docs/data-model.md b/docs/data-model.md index be00ee6..7bf690d 100644 --- a/docs/data-model.md +++ b/docs/data-model.md @@ -61,8 +61,8 @@ Metadata Documents", "RFC 9207 `iss` validation", "Resource indicators (RFC 8707 | `category` | enum | `discovery`, `registration`, `authorization`, `token`, `session`, `transport`, `delegation`, `consent` | | `description` | string | | | `supersedes` | string? | Feature id this replaces (e.g. CIMD supersedes DCR as the default) | -| `observable` | enum | `passive`, `active`, `manual` — how support can be verified | -| `party` | enum | `mcp-server`, `authorization-server`, `client` — who the requirement is on | +| `observable` | enum | `passive`, `active`, `manual`: how support can be verified | +| `party` | enum | `mcp-server`, `authorization-server`, `client`: who the requirement is on | | | | (`levelNote` lives inside each `revisions` entry) | ### Relation @@ -91,7 +91,7 @@ A public MCP server (or, later, another agent endpoint) that the MCP Atlas track | `url` | url | MCP endpoint | | `homepage` | url? | | | `transport` | enum | `streamable-http`, `sse`, `stdio-only` | -| `listed` | enum | `opt-in`, `public-directory`, `maintainer-added` — why it is in the atlas | +| `listed` | enum | `opt-in`, `public-directory`, `maintainer-added`: why it is in the atlas | | `source` | url | Where the listing came from; required unless `opt-in` | | `access` | Classification? | Who can connect: `public` (anyone; where an account is needed, anyone can create one by self-service), `closed` (an existing contract, a tenant an admin must enable, a sales process or a private beta stands in the way; a catalog of approved clients is not an obstacle to the person and stays in the evidence note), `unknown` | | `auth` | Classification? | How the endpoint authenticates clients: `oauth` (an OAuth 2.x authorization through the server's authorization server), `api-key` (a static credential the user pastes into the client), `none`, `unknown` | diff --git a/docs/development.md b/docs/development.md index 7f36f70..be79b6c 100644 --- a/docs/development.md +++ b/docs/development.md @@ -33,7 +33,7 @@ set. | `pnpm typecheck` | `astro sync` and `tsc --noEmit`. | | `pnpm test` | Unit tests (Vitest) over `src/lib` and `src/scanner`. | | `pnpm test:e2e` | Playwright against `astro preview`; run `pnpm build` first. | -| `pnpm check:prose [--fail]` | Lists copy in `src/pages`, `src/components`, `README.md` and `docs/*.md` that breaks the mechanical voice rules (banned words, em dash). Warns only until the content pass; `--fail` makes hits fail. | +| `pnpm check:prose` | Lists copy in `src/pages`, `src/components`, `README.md` and `docs/*.md` that breaks the mechanical voice rules (banned words, em dash) and fails on any hit. | | `pnpm check:budgets [dir]` | Byte budgets over `dist/` (gzip level 6): CSS, JavaScript, HTML, inline SVG and preloaded fonts per page, plus each island. Run `pnpm build` first. | | `pnpm check:contrast [--markdown] [--hex]` | WCAG contrast of the colour pairs in `src/styles/tokens.css`, light and dark; fails under 4.5:1 for text and 3:1 for glyphs and outlines. `--markdown` prints the table in `docs/design.md`, `--hex` the sRGB value of each token. | | `pnpm check` | validate:data, lint, check:prose, check:contrast, typecheck, unit tests, build, check:links, check:budgets. Must pass before a push. | @@ -58,8 +58,8 @@ set. `enforce` flag in `LIMITS` at the top of `scripts/check-budgets.ts`, and the package that meets a target flips it. - **Prose lint** (`pnpm check:prose`) reports the voice rules a script can check, from section 8 - of the same plan. It prints `file:line: rule: text` and exits 0. Set `FAIL` in - `scripts/check-prose.ts` to make it a gate. + of the same plan. It prints `file:line: rule: text` and fails on any hit, so `pnpm check` + fails too. A quotation of a source may use "ecosystem"; nothing else is exempt. - **Contrast** (`pnpm check:contrast`) recomputes every colour pair in `docs/design.md` from the OKLCH values in `tokens.css` and fails when one is under its minimum. diff --git a/docs/roadmap.md b/docs/roadmap.md index 8776a7c..4734d64 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -13,7 +13,7 @@ features and the review point after each. Each feature gets its own spec under ` | 006 | **Agents under EU rules** | eIDAS 2.0 and EUDI wallet mandates, AI Act oversight, NIS2 duties, as explorer scenarios and long-form pages. | Planned | | 007 | **Changelog and newsletter** | Generated from merged data and content changes. Runs once there is something to report. | Planned | | 008 | **Discovery challenge check** | The first nightly run withheld grades for servers that publish no protected resource metadata at a well-known URL (Atlassian, Plaid, Stack Overflow), because the other allowed mechanism, a `resource_metadata` pointer in the 401 `WWW-Authenticate` challenge, needs a request the passive scanner does not make. One unauthenticated request per server to its MCP endpoint, reading only the challenge header, would fill `mcp.prm.www-authenticate` beside `mcp.prm.well-known`, so the atlas shows which mechanism each server offers, or both. It is the first request beyond well-known metadata, so the spec must justify it against the constitution before it runs. | Implemented (pull request open); `specs/008-discovery-challenge/` records why it is still passive | -| 009 | **Server access and auth classes** | Today the atlas mixes servers anyone can connect to with ones that need an account, tenant or enterprise contract, and OAuth servers with API-key and open ones, using only the directory's unverified `authClaimed` label. Make access (public or closed) and auth (OAuth, API key, none) first-class, evidenced fields; let the atlas group and filter by them; and let the scanner and the grade use them; drop the label. | Implemented; `specs/009-access-auth-classes/` | +| 009 | **Server access and auth classes** | Today the atlas mixes servers anyone can connect to with ones that need an account, tenant or enterprise contract, and OAuth servers with API-key and open ones, using only the directory's unverified `authClaimed` label. Make access (public or closed) and auth (OAuth, API key, none) evidenced fields of their own; let the atlas group and filter by them; and let the scanner and the grade use them; drop the label. | Implemented; `specs/009-access-auth-classes/` | | 010 | **Composable discovery strategies** | After the first nightly runs, several requirements stayed `unknown` for every server for the same few reasons: streamable HTTP servers answer the scanner's `GET` with 405 before authenticating, listed `/sse` endpoints had moved, metadata documents name no scopes, and the `scope` parameter the endpoint already sends was not read. Make each check an ordered list of strategies over the sources the probe found, make the endpoint request a ladder, read the scope challenge, and let the run summary flag stale URLs. | Implemented; `specs/010-discovery-strategies/` | | 016 | **Visual redesign** | The site works but looks generated: boxes, shadows, glow, a slogan hero. Move it to one calm system shared with the maintainer's blog (flat surfaces, hairlines, two brand accents, Geist), with motion only where it explains a state, tighter byte and Lighthouse budgets, and a copy pass. No change to data, schema or URLs. | Planned; `specs/016-visual-redesign/plan.md` | diff --git a/scripts/check-prose.ts b/scripts/check-prose.ts index 288200c..5ebdee0 100644 --- a/scripts/check-prose.ts +++ b/scripts/check-prose.ts @@ -2,8 +2,8 @@ import { existsSync, readdirSync, readFileSync } from "node:fs"; import { join, relative } from "node:path"; // Mechanical rules from the voice checklist (specs/016-visual-redesign/plan.md section 8, rule 4). -// Warn-only until WP9 rewrites the copy: then set FAIL to true (or pass --fail meanwhile). -const FAIL = false; +// A gate since WP9 rewrote the copy: any hit fails `pnpm check`. +const FAIL = true; const root = process.cwd(); const fail = FAIL || process.argv.includes("--fail"); @@ -20,6 +20,8 @@ const WORDS = [ "cutting-edge", "next level", "game-changing", + "game-changer", + "holistic", "in the era of", "in the age of", "measured instead of asserted", @@ -114,5 +116,5 @@ if (hits.length) { for (const [name, count] of [...perFile].sort((a, b) => b[1] - a[1])) log(` ${String(count).padStart(4)} ${name}`); if (fail) process.exitCode = 1; - else console.log("(warn only; WP9 switches this check to fail)"); + else console.log("(warn only)"); } else console.log(`Prose OK: ${files.length} files`); diff --git a/specs/016-visual-redesign/tasks.md b/specs/016-visual-redesign/tasks.md index 61eb851..2d5b980 100644 --- a/specs/016-visual-redesign/tasks.md +++ b/specs/016-visual-redesign/tasks.md @@ -157,6 +157,21 @@ reveal, the token rail and the toggle rails; breach pages on `.rows`, `.panel` a - [x] W6-T13 Breach layout rework after review: the entry page puts the account (summary after its first sentence, the missing control, the replay as a rail in the toggle's ink) at reading width beside a sticky facts panel (timeline, affects, sources as short labels); the index is a ledger with the date, class and id in a narrow column beside each entry and its missing control and replay as labelled lines - [x] W6-T12 Screenshot baselines for `explorer.spec.ts` and `breaches.spec.ts`: refreshed through the `refresh-screenshots` label on the pull request +## WP9: Content pass + +The copy that WP3 to WP7 did not rewrite, against plan section 8 and the Plain English layer of +the review (sentences under 20 words on average and none over 30, active voice with a named +actor, "you" and "we", no em dashes or semicolons in body copy, no triads, no "X, not Y"). +Taken as review follow-up 9. Legal content keeps its substance; only the wording changes. + +- [x] W9-T01 Section-root h1 under 24 characters: `/mcp/` (and the revision pages, which share `MatrixPage.astro`), `/standards/`, `/explorer/`, `/breaches/`, `/mcp/scanner/`; the lede carries what the old headline said +- [x] W9-T02 `src/layouts/Base.astro`: the home `` without the em dash; the footer statement without "in the agent era" and without the triad +- [x] W9-T03 `src/pages/accessibility.astro`: a motion section that lists every animation of plan section 4 in plain words with its reduced-motion form, including Play and auto-advance in the explorer and the cell dialog; the known limits match the matrix on phones (R-05) +- [x] W9-T04 `README.md` and `docs/*.md` pass `check:prose`; `FAIL` is true in `scripts/check-prose.ts`, which also bans "game-changer" and "holistic" from the review's list; `docs/development.md` says the check fails +- [x] W9-T05 `/about/`, `/contribute/`, `/made-in-europe/`: one idea per sentence, no semicolons, no "X, not Y" principle names, no "yours to download" +- [x] W9-T06 `/privacy/`, `/imprint/`, `/disclaimer/`, `/404`: wording only, no semicolons in body copy, headings without the colon-and-triad pattern; legal content unchanged +- [ ] W9-T07 Screenshot baselines for the pages whose head changed, through the `refresh-screenshots` label + ## Review follow-ups (21 October 2026) Items from the redesign review, one pull request each, in order. @@ -170,3 +185,4 @@ Items from the redesign review, one pull request each, in order. - [x] R-06 Matrix on desktop: the table comes straight after the head, and the status key and the live count sit under it. "Filter and group" folds into one quiet button at the end of the head's meta line; opened, the form runs under the whole line. The revision is named once, in the meta line: the lede says "the MCP Authorization specification", the link to its text is "Specification text", and the published date shows only when it differs from the revision id. On wide screens the table fits the column instead of scrolling past its edge (at 1440 px three groups were hidden behind a clipped "Tra…"): its widths come from the group row, a group whose label is wider than its columns (`data-narrow`, from an estimate of the label's length) takes the label's width, and the other columns share the rest down to a 2.25rem floor, below which the frame scrolls. The island redoes the sums when a filter changes the groups. Phones keep R-05. Baselines through the `refresh-screenshots` label - [x] R-07 Home figures and copy: the facts read Servers, Requirements, Cells observed and Specifications. "Cells observed" counts the current matrix cells that have an observation, beside the total (611 of 897), in place of the 1,664 entries of the append-only history, which counted every scan of the same cell again; `Facts.astro` takes an optional `of` total that stands still while the figure counts up. "Latest observations" shows the six newest observations of the current revision with one row per requirement and per server, so one scan run no longer fills the panel with six rows of the same feature. The seed-data sentence under the list is gone (the server page and `/contribute/` no longer carried one). No baseline changes: the home screenshot stops above the facts - [x] R-08 The explorer as a scene: one Web Animations API timeline (a target-less scene clock) holds the playhead for Play, Pause, Previous, Next, a scrub bar in the stepper, the arrow keys, a click on a step and the decision log (M6). Auto-advance starts once the chain has sat in view for 1 s with no input, stops for good on hover, touch, focus, scroll or a key, never restarts and never runs under reduced motion; M6 in the plan is amended. Per step the sender and receiver light up in the actor head, the message draws in over 300ms, earlier steps keep a quieter arrow and later ones a hairline (colour, not opacity, so axe passes while stepping) (M5). The changed claims get their rail 40ms apart once the message lands (M7), now through the Web Animations API instead of an `is-fresh` class. A toggle swaps the chain in place through a same-document view transition of 220ms and pushes its URL; back and forward swap again, the analytics count the swap as a pageview (M17). A quiet log under the chain gains one line per step (M18). Durations come from the motion tokens (`--dur-message`, `--stagger` and `--dur-step` are new) through `durationToken()` in `src/lib/motion.ts`. Baselines through the `refresh-screenshots` label +- [x] R-09 Content pass (WP9 above): section-root headlines under 24 characters, the title suffix and footer statement, an accessibility statement that lists every motion with its reduced-motion form, `check:prose` as a gate, and the about, contribute, made-in-Europe, legal and 404 pages in plain English diff --git a/src/components/MatrixPage.astro b/src/components/MatrixPage.astro index c2fe9c9..1de9b1b 100644 --- a/src/components/MatrixPage.astro +++ b/src/components/MatrixPage.astro @@ -66,7 +66,7 @@ const classWords = (counts: { value: string; count: number }[], label: Record<st <div class="wrap"> <header class="page-head"> <p class="eyebrow">Servers</p> - <h1>Who supports what in <span class="nowrap">MCP Authorization</span></h1> + <h1>Who supports what</h1> <p class="lede"> The public MCP servers we list, against what the MCP Authorization specification requires of servers and their authorization servers. Each cell opens the observation behind it. @@ -266,10 +266,6 @@ const classWords = (counts: { value: string; count: number }[], label: Record<st </div> <style> - .nowrap { - white-space: nowrap; - } - /* the revision the matrix is measured against, and the way to the others, as the head's meta line */ .revision-line { column-gap: var(--space-5); diff --git a/src/layouts/Base.astro b/src/layouts/Base.astro index f1c3770..dc19314 100644 --- a/src/layouts/Base.astro +++ b/src/layouts/Base.astro @@ -22,7 +22,7 @@ const site = Astro.site ?? new URL("https://letsauth.eu"); const canonical = new URL(path ?? Astro.url.pathname, site); const plausibleDomain = import.meta.env.PUBLIC_PLAUSIBLE_DOMAIN as string | undefined; const fullTitle = - title === "letsauth" ? "letsauth — the lab for agent auth" : `${title} · letsauth`; + title === "letsauth" ? "letsauth · the lab for agent authorization" : `${title} · letsauth`; const here = path ?? Astro.url.pathname; // Footer link groups: what you can read, what you can take, and who is behind it. const footerGroups = [ @@ -184,9 +184,8 @@ const nav = [ <BrandLogo /> </a> <p> - An independent lab for authentication in the agent era, by {LAB}. Every cell cites its - source, the data is open, and the only thing that contacts a server is a scheduled scan - of public metadata. + An independent lab for agent authorization, run by {LAB}. Each cell cites its source + and the data is open. Only a nightly scan of public metadata ever contacts a server. </p> <a class="site-footer__eu" href="/made-in-europe/">Made in Europe, hosted in the EU</a> </div> diff --git a/src/pages/404.astro b/src/pages/404.astro index 6d94900..ea1e467 100644 --- a/src/pages/404.astro +++ b/src/pages/404.astro @@ -13,7 +13,7 @@ import Base from "@/layouts/Base.astro"; <p class="eyebrow">404</p> <h1>No page at this address</h1> <p class="lede"> - The link may be old, or the address mistyped. An id in the atlas stays stable once + The link may be old, or the address may have a typo. Ids in the atlas never change once published, so a missing server or feature page usually means a typo. </p> </header> @@ -23,7 +23,7 @@ import Base from "@/layouts/Base.astro"; <a class="button quiet" href="/standards/">Standards</a> </p> <p class="muted"> - Followed a broken link on this site? Please{" "} + If a link on this site brought you here, please{" "} <a href="https://github.com/letsauthlab/letsauth/issues/new" rel="noopener">report it</a>. </p> </div> diff --git a/src/pages/about.astro b/src/pages/about.astro index 71a9ac3..354bf87 100644 --- a/src/pages/about.astro +++ b/src/pages/about.astro @@ -25,44 +25,44 @@ const products = [ name: "Delegation chain explorer", href: "/explorer/", status: "Live", - text: "Step-through scenarios with the decoded token at each hop, and toggles for the attacks and misconfigurations that break them.", + text: "Scenarios you play step by step, with the decoded token at each hop. Toggles add the attacks and misconfigurations that break a chain.", }, { name: "Breach anatomy", href: "/breaches/", status: "Live", - text: "Published agent-auth vulnerabilities and incidents, each traced to the missing control, the atlas rows that name it and a replay in the explorer.", + text: "Published vulnerabilities and incidents in agent authorization. Each one names the control that was missing and links to the atlas rows and the explorer replay for it.", }, { name: "Agents under EU rules", status: "Planned", - text: "What eIDAS 2.0 and the EUDI wallet, the AI Act and NIS2 ask of agent delegation, as scenarios and long reads.", + text: "Scenarios and long reads on what eIDAS 2.0 and the EUDI wallet, the AI Act and NIS2 ask of agent delegation.", }, ]; const principles = [ { - name: "Cited, not asserted", - text: "A claim without a specification revision and section does not ship.", + name: "Cited", + text: "Each claim names a specification revision and section. A claim without one does not ship.", }, { name: "Neutral", - text: "Nothing is ranked or scored. Rows are alphabetical, and no vendor pays for anything.", + text: "We rank and score nothing. Rows are in alphabetical order, and no vendor pays for anything.", }, { - name: "Unknown is honest", - text: "A missing observation reads <em>unknown</em>, never failure.", + name: "Unknown means no observation", + text: "A cell with no observation on record reads <em>unknown</em>. It never reads as a failure.", }, { - name: "Passive by default", - text: "We read public metadata only, on a schedule, never on a visitor's request, and never with credentials.", + name: "Passive", + text: "We read public metadata only. A scheduled job does the reading, without credentials. A visitor can never start a scan.", }, { name: "Open", - text: "Code under MIT, data under CC BY 4.0, history kept in a public repository where anyone can propose a correction.", + text: "The code is MIT and the data is CC BY 4.0. The history is in a public repository, where anyone can propose a correction.", }, { - name: "European by default", - text: 'Built in Berlin, hosted and measured in the EU; see <a href="/made-in-europe/">made in Europe</a>.', + name: "European", + text: 'We build in Berlin, and EU companies host and measure the site. <a href="/made-in-europe/">Made in Europe</a> lists every service.', }, ]; --- @@ -70,20 +70,22 @@ const principles = [ <Document title="About" heading={LAB} - description={`${LAB} is an independent, vendor-neutral lab for authentication and authorization in the agent era. letsauth.eu is where its work is published.`} + description={`${LAB} is an independent, vendor-neutral lab for how AI agents are authenticated and authorized. It publishes its work on letsauth.eu.`} path="/about/" eyebrow="About" - lede="An independent, vendor-neutral lab for how AI agents are authenticated and authorized. We measure what the protocols require and what real systems do, and publish all of it as open data." - updated="2026-10-14" + lede="An independent, vendor-neutral lab for how AI agents are authenticated and authorized. We record what the protocols require and what real servers do, and publish it all as open data." + updated="2026-10-22" > <h2>Why a lab</h2> <p> - Agents now call APIs, use tools and act for people. The rules for how they get and pass on - authority sit where agent protocols (MCP, A2A) meet identity standards (OAuth, OpenID - Connect, token exchange, workload identity), and both sides move monthly. Most of what is - written about it is assertion. {LAB} replaces assertion with evidence: every claim cites a - specification revision, every status cites a dated observation, and the data is yours to - download and check. + Agents now call APIs, use tools and act for people. The rules for how they get authority and + pass it on sit where agent protocols (MCP, A2A) meet identity standards (OAuth, OpenID + Connect, token exchange, workload identity). Both sides change every month. + </p> + <p> + Much of what is written about this rests on claims that nobody can check. {LAB} keeps the + evidence instead. Each requirement cites a specification revision, and each status cites a + dated observation. You can download all of the data and check it yourself. </p> <h2>What we build</h2> @@ -101,8 +103,9 @@ const principles = [ } </ul> <p> - All of them are views on one shared data model. The order and status of each is in the{" "} - <a href={`${REPO}/blob/main/docs/roadmap.md`} rel="noopener">roadmap</a>. + All of them read from one shared data model. The{" "} + <a href={`${REPO}/blob/main/docs/roadmap.md`} rel="noopener">roadmap</a> shows what comes + next. </p> <h2>How we work</h2> @@ -120,16 +123,16 @@ const principles = [ <h2>Who is behind it</h2> <p> {LAB} is run by <a href={OPERATOR.site} rel="noopener">{OPERATOR.name}</a>, a security - engineer in Berlin who works on usable security and web authentication. It is a personal, - independent project; the <a href="/disclaimer/">disclaimer</a> says what that means for - affiliation. + engineer in Berlin who works on usable security and web authentication. The lab is a + personal, independent project. The <a href="/disclaimer/">disclaimer</a> says what that + means for affiliation. </p> <h2>Get involved</h2> <p> - Know how a server behaves? <a href="/contribute/">Add an observation</a>. Found a - mistake, or want a specification on the map? Open an issue in the{" "} - <a href={REPO} rel="noopener">repository</a>. Anything else:{" "} + If you know how a server behaves, <a href="/contribute/">add an observation</a>. If you + found a mistake or want a specification on the map, open an issue in the{" "} + <a href={REPO} rel="noopener">repository</a>. For anything else, write to{" "} <a href={`mailto:${OPERATOR.email}`}>{OPERATOR.email}</a>. </p> diff --git a/src/pages/accessibility.astro b/src/pages/accessibility.astro index 2503e14..31800ff 100644 --- a/src/pages/accessibility.astro +++ b/src/pages/accessibility.astro @@ -1,50 +1,150 @@ --- import Document from "@/layouts/Document.astro"; import { OPERATOR, REPO } from "@/lib/site"; + +/** Every animation on the site (plan 016, section 4) and what you see with reduced motion. */ +const motions = [ + { + where: "Moving between pages", + what: "The page crossfades in 220 ms. The matrix, the map and a chain's title morph into the page you open.", + reduced: "The new page appears at once.", + }, + { + where: "Home and server pages", + what: "The figures count up from zero once, when they scroll into view.", + reduced: "The figures show their value at once.", + }, + { + where: "Standards map", + what: "The longest builds-on path draws itself in once, with a soft halo that fades after.", + reduced: "The path is drawn complete, with no halo.", + }, + { + where: "Explorer: steps", + what: "On each step the sender and receiver light up and the message arrow draws in over 300 ms. The changed token claims get a rail that fades over 480 ms.", + reduced: "The step, its arrow and its rails appear at once.", + }, + { + where: "Explorer: Play and auto-advance", + what: "Play moves to the next step every 2.5 seconds. When a chain sits in view for a second, it starts by itself once. It stops for good when you hover, touch, focus, scroll or press a key, and Pause stops it too.", + reduced: + "There is no Play button and nothing starts by itself. Previous, Next, the scrub bar and the step list still work.", + }, + { + where: "Explorer: toggles", + what: "A toggle swaps the chain in place in 220 ms. The steps it touched get their rail one after another.", + reduced: "The toggle opens as a normal page and the rails show at once.", + }, + { + where: "Explorer: decision log", + what: "Each new line of the log under the chain fades in over 220 ms.", + reduced: "The lines appear at once.", + }, + { + where: "Matrix", + what: "The row and column under your pointer or focus take a tint. A cell opens a dialog that fades in and rises 8 px.", + reduced: "The tint and the dialog appear at once.", + }, + { + where: "Scanner report", + what: "The list of requests prints line by line, once, with the grade last.", + reduced: "All lines show at once.", + }, + { + where: "Breach pages", + what: "The timeline line draws from the top once and its dates fade in behind it.", + reduced: "The timeline shows complete.", + }, + { + where: "Long pages like this one", + what: "A thin bar at the top shows how far you have read.", + reduced: "There is no bar.", + }, + { + where: "Links and buttons", + what: "Colours and borders change in 120 ms on hover and focus.", + reduced: "They change at once.", + }, +]; --- <Document title="Accessibility" heading="Accessibility statement" - description="letsauth.eu aims to meet WCAG 2.2 level AA on every page. How that is checked, what is known to fall short, and how to report a barrier." + description="letsauth.eu aims to meet WCAG 2.2 level AA on every page. How we check it, every animation and its reduced-motion form, what falls short and how to report a barrier." path="/accessibility/" eyebrow="Accessibility" - lede="Every page on letsauth.eu should work for everyone: with a keyboard, a screen reader, zoom, either colour scheme, reduced motion and no JavaScript." - updated="2026-10-14" + lede="We want every page on letsauth.eu to work with a keyboard, a screen reader, zoom, either colour scheme, reduced motion and no JavaScript." + updated="2026-10-22" > <h2>The target</h2> <p> The site aims to meet the <a href="https://www.w3.org/TR/WCAG22/" rel="noopener">Web - Content Accessibility Guidelines (WCAG) 2.2</a> at level AA. This is a rule in the project's - constitution, not an aspiration: a change that breaks it does not merge. + Content Accessibility Guidelines (WCAG) 2.2</a> at level AA. The project's constitution + makes this a rule. A change that breaks it does not merge. </p> - <h2>How it is checked</h2> + <h2>How we check it</h2> <ul> - <li>Every pull request runs automated accessibility checks (axe) on the main pages, in light and dark and at phone width, and fails on any serious violation.</li> - <li>Lighthouse must score at least 95 for accessibility on the atlas pages.</li> - <li>Pages are pre-rendered HTML that read fully without JavaScript; interactive parts add to that, they do not replace it.</li> - <li>Status in the atlas is never shown by colour alone: each status has its own shape and text.</li> - <li>Animations are switched off when your system asks for reduced motion.</li> + <li>Every pull request runs automated accessibility checks (axe) on the main pages. They run in light and dark and at phone width, and fail on any serious violation.</li> + <li>Lighthouse must score at least 95 for accessibility on the main pages.</li> + <li>Pages are pre-rendered HTML and read fully without JavaScript. Interactive parts add to that HTML and never replace it.</li> + <li>Colour never carries a status alone. Each status in the atlas has its own shape and text.</li> + </ul> + + <h2>Motion</h2> + <p> + Every animation runs once per page or once per action, and none of them loops. When your + system asks for reduced motion, each one shows its end state at once. This list is every + animation on the site. + </p> + <ul class="rows motions" role="list"> + { + motions.map((m) => ( + <li> + <h3 class="rows__title">{m.where}</h3> + <p>{m.what}</p> + <p class="motions__reduced"> + <span class="motions__label">Reduced motion</span> {m.reduced} + </p> + </li> + )) + } </ul> <h2>Known limits</h2> <ul> <li> - The atlas matrix is a wide data table. On a small screen it scrolls sideways inside its - frame; each server and each requirement also has its own page that reads as plain text. + The server matrix is a wide data table. On a phone it shows one requirement group at a + time, and you scroll sideways inside its frame. Each server and each requirement also has + its own page that reads as plain text. </li> <li> - The standards map draws a graph. The same specifications and relations are listed as text - on each specification's page. + The standards map is a drawing. The same specifications and relations are also listed as + an outline under it, and on a phone the outline takes the drawing's place. </li> - <li>Automated checks find only part of the barriers. Manual testing with assistive technology is not yet systematic.</li> + <li>Automated checks find only some barriers. We do not yet test with assistive technology on a fixed schedule.</li> </ul> <h2>Report a barrier</h2> <p> - If something does not work for you, tell us what page, what you tried and what happened. + If something does not work for you, tell us the page, what you tried and what happened. Write to <a href={`mailto:${OPERATOR.email}`}>{OPERATOR.email}</a> or open an issue in the{" "} - <a href={REPO} rel="noopener">repository</a>. Barriers are treated as bugs. + <a href={REPO} rel="noopener">repository</a>. We treat barriers as bugs. </p> </Document> + +<style> + .motions p { + margin: 0; + } + .motions__reduced { + margin-top: var(--space-1); + font-size: var(--small); + color: var(--ink-2); + } + .motions__label { + font-weight: 500; + color: var(--ink); + } +</style> diff --git a/src/pages/breaches/index.astro b/src/pages/breaches/index.astro index 3946b1e..3ae63ad 100644 --- a/src/pages/breaches/index.astro +++ b/src/pages/breaches/index.astro @@ -25,7 +25,7 @@ const date = (d: string) => <div class="wrap"> <header class="page-head"> <p class="eyebrow">Breach anatomy</p> - <h1>Published failures in how agents were authorized</h1> + <h1>Agent auth failures</h1> <p class="lede"> Each entry is a vulnerability or incident written from its advisory, with the control that was missing and the MCP auth atlas requirements that stand for it. Where the diff --git a/src/pages/contribute.astro b/src/pages/contribute.astro index 91d3faa..11fafc9 100644 --- a/src/pages/contribute.astro +++ b/src/pages/contribute.astro @@ -21,11 +21,11 @@ const entry = `- server: stripe # must equal the file's serv <Document title="Contribute" - heading="Add what you know, with evidence" - description="How to add an observation, correct a cell, list a server or withdraw one from the MCP auth atlas. Two paths, both reviewed before anything is published." + heading="Add what you know" + description="How to add an observation, correct a cell, list a server or withdraw one from the MCP auth atlas. A maintainer reviews every change before it is published." path="/contribute/" eyebrow="Contribute" - lede="The atlas is a dataset in a git repository. Anyone can add an observation or correct one; a maintainer reviews the evidence before it is published. Nothing is ever edited in place." + lede="The atlas is a dataset in a git repository. You can add an observation or correct one, with evidence, and a maintainer reviews it before it goes live." > <h2>Two ways in</h2> <p> @@ -37,10 +37,10 @@ const entry = `- server: stripe # must equal the file's serv <p class="eyebrow">Path A</p> <h3 class="rows__title">Open an issue</h3> <p> - The quickest route. The form asks for the same fields as a data entry: server, feature, - revision, status, method, date, evidence and your handle. A maintainer turns an accepted - issue into a pull request and links the issue as evidence where the issue itself is the - evidence. + This is the quickest route. The form asks for the same fields as a data entry: server, + feature, revision, status, method, date, evidence and your handle. A maintainer turns an + accepted issue into a pull request. When the issue itself is the evidence, the entry + links to it. </p> <p><a class="button" href={issue} rel="noopener">Open the issue form</a></p> </li> @@ -48,8 +48,9 @@ const entry = `- server: stripe # must equal the file's serv <p class="eyebrow">Path B</p> <h3 class="rows__title">Open a pull request</h3> <p> - Append an entry to the server's observation file yourself. Validation runs locally and - again in CI, so a reviewer only has to open the evidence. The steps are below. + You append an entry to the server's observation file yourself. The checks run on your + machine and again in CI, so a reviewer only has to open the evidence. The steps are + below. </p> <p><a class="button quiet" href={repo} rel="noopener">Open the repository</a></p> </li> @@ -71,21 +72,23 @@ const entry = `- server: stripe # must equal the file's serv <h3>Entry format</h3> <pre tabindex="0"><code>{entry}</code></pre> <p> - <strong>Evidence</strong> is a URL a reviewer can open (a metadata document, a - documentation page, a response capture) or a precise note. <strong>Method</strong> says - how you know: <code>manual</code> for something you checked by hand, - <code>vendor-claim</code> for what the vendor says, <code>passive-scan</code> for reading - public metadata, <code>active-scan</code> for sending requests. When two observations of a - cell exist, the newest one is shown; equal timestamps are broken by method strength - (active-scan over passive-scan over manual over vendor-claim). + <strong>Evidence</strong> is a URL a reviewer can open, such as a metadata document, a + documentation page or a response capture. A precise note works too. + <strong>Method</strong> says how you know. Use <code>manual</code> for something you checked + by hand, <code>vendor-claim</code> for what the vendor says, <code>passive-scan</code> for + reading public metadata and <code>active-scan</code> for sending requests. + </p> + <p> + When a cell has two observations, the atlas shows the newest. If both have the same time, + the stronger method wins: active-scan, then passive-scan, then manual, then vendor-claim. </p> <h2>Let the scanner write it</h2> <p> - For the {scannerChecks} requirements the <a href="/mcp/scanner/">passive scanner</a> can read - from public metadata, prefer <code>pnpm scan <server-id> --write</code> over a hand-written - entry. It appends exactly the observations whose status or evidence changed, with evidence - anyone can reproduce by running the same command. The nightly run does the same for every + The <a href="/mcp/scanner/">passive scanner</a> can read {scannerChecks} requirements from + public metadata. For those, run <code>pnpm scan <server-id> --write</code> instead of + writing the entry by hand. It appends only the observations whose status or evidence changed, + and anyone can repeat the same command to check them. The nightly run does this for every listed server and opens its own pull request. </p> @@ -93,7 +96,7 @@ const entry = `- server: stripe # must equal the file's serv <div class="callout"> <p class="callout__title">Observations are never edited or removed</p> <p> - A correction is a new entry with a newer <code>observedAt</code>; the old one stays in the + A correction is a new entry with a newer <code>observedAt</code>. The old one stays in the history on the server page. CI fails any pull request that changes or deletes an existing entry. </p> @@ -101,24 +104,24 @@ const entry = `- server: stripe # must equal the file's serv <h2>Listing a server</h2> <p> - Add <code>data/servers/<id>.yaml</code> with the endpoint, transport and - <code>listed</code>. If you maintain the server, use <code>listed: opt-in</code>; otherwise - give the <code>source</code> that lists it publicly. Servers are listed as they appear in - public directories; nobody is scored or ranked. + Add <code>data/servers/<id>.yaml</code> with the endpoint, the transport and + <code>listed</code>. If you maintain the server, use <code>listed: opt-in</code>. If not, + give the <code>source</code> that lists it publicly. We list servers as they appear in public + directories, and we score or rank nobody. </p> <h2>Withdrawing a server</h2> <p> - If you maintain a server and do not want it listed, use the issue form with - "withdraw my server" and a way to verify that you maintain it. A pull request then sets - <code>withdrawn</code> on the server. It leaves the matrix and the export; its observations - stay in the repository history. + If you maintain a server and do not want it listed, use the issue form. Choose "withdraw my + server" and give us a way to check that you maintain it. A pull request then sets + <code>withdrawn</code> on the server. It leaves the matrix and the export, and its + observations stay in the repository history. </p> <h2>Licences</h2> <p> - Data you contribute is published under <a href={`${repo}/blob/main/LICENSE-DATA`} rel="noopener">CC BY 4.0</a>; - the site's code is <a href={`${repo}/blob/main/LICENSE`} rel="noopener">MIT</a>. + Data you contribute is published under <a href={`${repo}/blob/main/LICENSE-DATA`} rel="noopener">CC BY 4.0</a>. + The site's code is <a href={`${repo}/blob/main/LICENSE`} rel="noopener">MIT</a>. </p> <Fragment slot="aside"> diff --git a/src/pages/disclaimer.astro b/src/pages/disclaimer.astro index 9fec6f8..14f846b 100644 --- a/src/pages/disclaimer.astro +++ b/src/pages/disclaimer.astro @@ -5,18 +5,18 @@ import { LAB, OPERATOR } from "@/lib/site"; <Document title="Disclaimer" - heading="Disclaimer: independent, unaffiliated, no advice" - description="letsauth.eu is an independent project. It does not speak for any employer or vendor, takes no money for listings, and its data is reference material, not security advice or certification." + heading="Disclaimer" + description="letsauth.eu is an independent project. It does not speak for any employer or vendor and takes no money for listings. Its data is reference material, with no security advice and no certification." path="/disclaimer/" eyebrow="Legal" - lede={`${LAB} is an independent project. It speaks for no employer and no vendor, and what it publishes is reference material, not advice.`} - updated="2026-10-14" + lede={`${LAB} is an independent project. It speaks for no employer and no vendor. What it publishes is reference material and gives no advice.`} + updated="2026-10-22" > <h2>No employer affiliation</h2> <p> letsauth is a personal project. I, {OPERATOR.name}, build it in my own time to make agent - authentication easier to understand, and everything here reflects my own views and my - experience in usable security and web authentication. I do not speak for the company I work + authentication easier to understand. Everything here reflects my own views and my experience + in usable security and web authentication. I do not speak for the company I work for, or for any company I worked for in the past, and nothing on this site is an official position of any of them. </p> @@ -35,27 +35,27 @@ import { LAB, OPERATOR } from "@/lib/site"; <h2>Reference material, not advice</h2> <p> - The atlases are compiled with care from specifications, vendor documentation, public + We compile the atlases with care from specifications, vendor documentation, public directories and passive reads of public metadata. Each cell shows its source, method and - date. Products change and observations age, so a cell can be out of date or wrong, and an{" "} - <em>unknown</em> cell says only that nothing has been observed. The site is not a security - audit, a certification or a compliance statement for any product, and it does not replace - your own assessment. Please <a href="/contribute/">send a correction</a> when you + date. Products change and observations age, so a cell can be out of date or wrong. An{" "} + <em>unknown</em> cell says only that no observation is on record. The site is not a security + audit, a certification or a compliance statement for any product. It does not replace your + own assessment. Please <a href="/contribute/">send a correction</a> when you find a mistake. </p> <h2>Liability</h2> <p> The content is provided as is, without warranty. Liability for its accuracy, completeness - or timeliness is excluded as far as the law permits; liability for intent and gross - negligence, and for injury to life, body or health, is unaffected. + or timeliness is excluded as far as the law permits. This does not affect liability for + intent and gross negligence, or for injury to life, body or health. </p> <h2>External links</h2> <p> The site links to specifications, vendor pages and repositories run by others. Their owners are responsible for them. When a link was added, the target showed no apparent - breach of law; we do not monitor linked pages continuously. If you notice a link to unlawful + breach of law. We do not monitor linked pages continuously. If you notice a link to unlawful content, tell us and we will remove it. </p> diff --git a/src/pages/explorer/index.astro b/src/pages/explorer/index.astro index ba6b900..2f4c7fe 100644 --- a/src/pages/explorer/index.astro +++ b/src/pages/explorer/index.astro @@ -27,7 +27,7 @@ for (const { scenario } of scenarios) { <div class="wrap"> <header class="page-head"> <p class="eyebrow">Delegation chain explorer</p> - <h1>How authority moves from a person through agents to an API</h1> + <h1>Delegation chains</h1> <p class="lede"> Each scenario is a chain of requests between a user, agents, MCP servers and authorization servers, written from the specification sections it cites. Every step diff --git a/src/pages/imprint.astro b/src/pages/imprint.astro index 0d12fd9..5aedb8f 100644 --- a/src/pages/imprint.astro +++ b/src/pages/imprint.astro @@ -10,7 +10,7 @@ import { LAB, OPERATOR, REPO } from "@/lib/site"; path="/imprint/" eyebrow="Legal" lede={`letsauth.eu is published by ${LAB}, an independent project run by one person in Berlin.`} - updated="2026-10-14" + updated="2026-10-22" > <h2>Provider</h2> <p>Information under § 5 of the German Digital Services Act (DDG).</p> @@ -28,8 +28,8 @@ import { LAB, OPERATOR, REPO } from "@/lib/site"; </p> <p> For corrections to the atlas data, the quickest route is an issue or a pull request in the{" "} - <a href={REPO} rel="noopener">public repository</a>; see <a href="/contribute/">how - to contribute</a>. To report a security problem with this site, follow the{" "} + <a href={REPO} rel="noopener">public repository</a>. The <a href="/contribute/">contribute + page</a> explains how. To report a security problem with this site, follow the{" "} <a href="/.well-known/security.txt">security.txt</a> instructions. </p> @@ -50,8 +50,8 @@ import { LAB, OPERATOR, REPO } from "@/lib/site"; <h2>More</h2> <p> - How this site handles data is in the <a href="/privacy/">privacy note</a>. What the site is - and is not, and the limits of liability, are in the <a href="/disclaimer/">disclaimer</a>. + The <a href="/privacy/">privacy note</a> says how this site handles data. The{" "} + <a href="/disclaimer/">disclaimer</a> says what the site is and the limits of liability. </p> </Document> diff --git a/src/pages/made-in-europe.astro b/src/pages/made-in-europe.astro index d2b84f6..ffc1702 100644 --- a/src/pages/made-in-europe.astro +++ b/src/pages/made-in-europe.astro @@ -32,7 +32,7 @@ const stack: { what: string; who: string; note: string; where: keyof typeof WHER { what: "Fonts", who: "Served from letsauth.eu itself", - note: "Geist and Geist Mono are open-source fonts bundled with the site; no font service is contacted.", + note: "Geist and Geist Mono are open-source fonts that ship with the site. No font service is contacted.", where: "eu", }, { @@ -56,16 +56,16 @@ const stack: { what: string; who: string; note: string; where: keyof typeof WHER path="/made-in-europe/" eyebrow="Made in Europe" heading="Made in Europe, hosted in the EU" - lede="letsauth is built in Berlin and runs on European infrastructure: EU hosting, EU analytics, an .eu domain. Here is every service the site depends on, including the one that is not European." - updated="2026-10-14" + lede="We build letsauth in Berlin and run it on European services. This page lists every service the site depends on, including the one outside the EU." + updated="2026-10-22" > <h2>Why it matters here</h2> <p> - This lab studies who gets to act on whose behalf. Where your data goes, and under which law, - is part of the same question. European rules (the GDPR, eIDAS 2.0 and the EUDI wallet, the - AI Act, NIS2) shape how agents may be delegated authority, so the site that writes about them - keeps to them too: no cookies that need consent, visitor data handled only by EU companies, - and no third-party scripts beyond EU analytics. + This lab studies who may act on whose behalf. Where your data goes, and under which law, is + part of the same question. European rules shape how agents may receive authority: the GDPR, + eIDAS 2.0 and the EUDI wallet, the AI Act and NIS2. A site that writes about them should keep + to them. So the site sets no cookies that need consent, and only EU companies handle visitor + data. The only third-party script is the EU analytics. </p> <h2>What the site runs on</h2> @@ -88,19 +88,20 @@ const stack: { what: string; who: string; note: string; where: keyof typeof WHER <h2>The exception</h2> <p> - The code, the data and the automated checks live on GitHub, a US company, because that is where open-source - contributors already are and where a pull request can be reviewed in public. Visitors to - letsauth.eu never touch it: no script, font or image on the site is loaded from GitHub. If - you contribute, GitHub's terms apply to what you do there; the{" "} + The code, the data and the automated checks live on GitHub, a US company. We use it because + open-source contributors are already there and a pull request can be reviewed in public. + Visitors to letsauth.eu never touch it, because the site loads no script, font or image from + GitHub. If you contribute, GitHub's terms apply to what you do there, and the{" "} <a href="/privacy/">privacy note</a> has the details. The whole history is in git, so the project can move to a European forge without losing anything. </p> <h2>Check it yourself</h2> <p> - Open your browser's network panel on any page: every request goes to letsauth.eu. Even the - analytics count is sent there and forwarded to Plausible by the CDN. The deployment and hosting setup is documented in the{" "} - <a href={`${REPO}/blob/main/docs/deploy.md`} rel="noopener">repository</a>. + Open your browser's network panel on any page. Every request goes to letsauth.eu, even the + analytics count, which the CDN forwards to Plausible. The{" "} + <a href={`${REPO}/blob/main/docs/deploy.md`} rel="noopener">deployment guide</a> in the + repository documents the hosting setup. </p> </Document> diff --git a/src/pages/mcp/scanner/index.astro b/src/pages/mcp/scanner/index.astro index 33eda85..58bd5a5 100644 --- a/src/pages/mcp/scanner/index.astro +++ b/src/pages/mcp/scanner/index.astro @@ -62,13 +62,12 @@ const ladder = [ <Crumbs trail={[{ href: "/mcp/", label: "Servers" }]} current="Scanner" /> <header class="page-head"> <p class="eyebrow">Scanner</p> - <h1>What a server's metadata says, measured every night</h1> + <h1>The nightly scanner</h1> <p class="lede"> - The scanner reads the public authorization metadata an MCP server publishes for every - client and judges the {checks.length} requirements of MCP Authorization {revision.id} that - metadata can answer. Nothing else is sent: no token, no authorization flow, no protocol - message; the one request to the endpoint itself carries no credentials and only its - status and headers are read. Results land in the atlas as observations, by pull request. + Every night the scanner reads the public authorization metadata of each listed MCP server + and judges the {checks.length} requirements of MCP Authorization {revision.id} that this + metadata can answer. It sends no token and no credentials, and starts no authorization + flow. Its results reach the atlas as observations, by pull request. </p> <dl class="facts" aria-label="Scanner at a glance"> <div> diff --git a/src/pages/privacy.astro b/src/pages/privacy.astro index f3b8792..1b88baa 100644 --- a/src/pages/privacy.astro +++ b/src/pages/privacy.astro @@ -5,25 +5,25 @@ import { LAB, OPERATOR, REPO } from "@/lib/site"; <Document title="Privacy" - heading="Privacy note: no cookies, no accounts, no tracking" + heading="Privacy note" description="letsauth.eu sets no cookies and collects no personal data beyond cookieless, aggregate EU analytics. What is processed, by whom, and your rights under the GDPR." path="/privacy/" eyebrow="Legal" - lede="letsauth.eu sets no cookies, has no accounts and does not track you across sites. It counts page views with cookieless, aggregate analytics hosted in the EU. That is all." - updated="2026-10-14" + lede="letsauth.eu sets no cookies, has no accounts and does not track you across sites. It counts page views with cookieless, aggregate analytics hosted in the EU." + updated="2026-10-22" > <h2>In short</h2> <ul> <li>No cookies, no fingerprinting, no advertising, no social media embeds.</li> <li>No personal information is collected, sold, shared with advertisers or used for profiling.</li> - <li>Fonts, scripts and styles are served from this site; your browser contacts no third party to render a page.</li> + <li>Fonts, scripts and styles come from this site. Your browser contacts no third party to show a page.</li> <li> The one thing stored in your browser is your colour theme, and only if you choose one. It never leaves your device. </li> <li> - Nothing is scanned on a visitor's request. Scanning is a scheduled backend job over the - public metadata of listed servers; it involves no data about you. + Nothing is scanned on a visitor's request. A scheduled backend job reads the public + metadata of listed servers. It involves no data about you. </li> </ul> @@ -39,10 +39,9 @@ import { LAB, OPERATOR, REPO } from "@/lib/site"; <h2>Hosting and delivery</h2> <p> - The site is a set of static files served by Bunny.net (BunnyWay d.o.o., Ljubljana, - Slovenia) from a storage zone in the EU. To deliver a page, its servers necessarily process - your IP address, the requested address, the time, and the browser identification your - browser sends. Bunny.net processes this on our behalf under a data processing agreement and + Bunny.net (BunnyWay d.o.o., Ljubljana, Slovenia) serves the site as static files from a + storage zone in the EU. To deliver a page, its servers must process your IP address, the + requested address, the time and the browser identification your browser sends. Bunny.net processes this on our behalf under a data processing agreement and may keep it in short-lived technical logs for security and operation. We do not use it to identify or profile visitors. </p> @@ -51,8 +50,8 @@ import { LAB, OPERATOR, REPO } from "@/lib/site"; <h2>Analytics</h2> <p> We count visits with <a href="https://plausible.io/data-policy" rel="noopener">Plausible - Analytics</a> (Plausible Insights OÜ, Tartu, Estonia), which is built to work without - cookies or personal data and stores its data in the EU. It records, per page view: + Analytics</a> (Plausible Insights OÜ, Tartu, Estonia). It works without cookies or personal + data and stores its data in the EU. For each page view it records: </p> <ul> <li><strong>Page address</strong>, to see which pages are read and how often.</li> @@ -71,7 +70,8 @@ import { LAB, OPERATOR, REPO } from "@/lib/site"; <p> To count unique visitors per day without a cookie, Plausible combines the IP address and browser identification with a salt that changes every 24 hours and keeps only the resulting - hash; the salt is deleted, so a visitor cannot be recognised the next day or across sites. + hash. The salt is then deleted, so nobody can recognise a visitor the next day or across + sites. IP addresses are never stored. The statistics are aggregate and nobody can trace them back to you. </p> @@ -89,31 +89,31 @@ import { LAB, OPERATOR, REPO } from "@/lib/site"; settings. Storing it is strictly necessary for a function you asked for (§ 25 (2) no. 2 TDDDG). </p> - <p>Pages read fully without JavaScript; nothing else is stored.</p> + <p>Pages read fully without JavaScript. Nothing else is stored.</p> <h2>Scanning of MCP servers</h2> <p> - letsauth does not scan anything on a visitor's request, and there is no form that triggers a - scan. A scheduled job on the project's own backend (Scaleway, EU region) reads, once a night, the - public metadata of the MCP - servers listed in the atlas (the documents a server publishes for any client to read), with - no credentials and without starting any authorization flow. Its findings are published as - data in the <a href={REPO} rel="noopener">public repository</a>. This job processes - technical data about services, not personal data about visitors. Server maintainers can ask - for their server to be withdrawn; see <a href="/contribute/">how to contribute</a>. + letsauth scans nothing on a visitor's request, and no form can start a scan. Once a night, a + scheduled job on the project's own backend (Scaleway, EU region) reads the public metadata of + the MCP servers listed in the atlas. These are the documents a server publishes for any + client to read. The job uses no credentials and starts no authorization flow. Its findings + are published as data in the <a href={REPO} rel="noopener">public repository</a>. The job + processes technical data about services and no personal data about visitors. Server + maintainers can ask for their server to be withdrawn, as <a href="/contribute/">the + contribute page</a> describes. </p> <h2>Contributions and GitHub</h2> <p> - Corrections and new data go through issues and pull requests on GitHub (GitHub, Inc., USA). + Corrections and new data arrive as issues and pull requests on GitHub (GitHub, Inc., USA). When you contribute there, GitHub's{" "} <a href="https://docs.github.com/site-policy/privacy-policies/github-general-privacy-statement" rel="noopener">privacy statement</a>{" "} applies. The handle you give in an observation (<code>observedBy</code>) becomes part of the public dataset under CC BY 4.0, which requires attribution. If you want it removed, write to - us and we will act on your request under Art. 17 GDPR; copies in the public history of the - repository or made by others may remain. + us and we will act on your request under Art. 17 GDPR. Copies in the public history of the + repository, or copies others made, may remain. </p> - <p>Links to GitHub and to specification publishers are plain links; nothing is loaded from them until you follow one.</p> + <p>Links to GitHub and to specification publishers are plain links. Nothing loads from them until you follow one.</p> <h2>Email</h2> <p> @@ -125,10 +125,10 @@ import { LAB, OPERATOR, REPO } from "@/lib/site"; <h2>Your rights</h2> <p>Under the GDPR you have the right to:</p> <ul> - <li>access the personal data we hold about you (Art. 15);</li> - <li>have it corrected (Art. 16) or erased (Art. 17);</li> - <li>restrict its processing (Art. 18) and receive it in a portable format (Art. 20);</li> - <li>object to processing based on legitimate interest (Art. 21).</li> + <li>access the personal data we hold about you (Art. 15)</li> + <li>have it corrected (Art. 16) or erased (Art. 17)</li> + <li>restrict its processing (Art. 18) and receive it in a portable format (Art. 20)</li> + <li>object to processing based on legitimate interest (Art. 21)</li> </ul> <p> Write to <a href={`mailto:${OPERATOR.email}`}>{OPERATOR.email}</a>. You can also complain diff --git a/tests/e2e/__screenshots__/breaches.spec.ts/breaches-phone.png b/tests/e2e/__screenshots__/breaches.spec.ts/breaches-phone.png index 02d8130..edbd048 100644 Binary files a/tests/e2e/__screenshots__/breaches.spec.ts/breaches-phone.png and b/tests/e2e/__screenshots__/breaches.spec.ts/breaches-phone.png differ diff --git a/tests/e2e/__screenshots__/explorer.spec.ts/explorer-dark.png b/tests/e2e/__screenshots__/explorer.spec.ts/explorer-dark.png index 7ecaaea..80de5c3 100644 Binary files a/tests/e2e/__screenshots__/explorer.spec.ts/explorer-dark.png and b/tests/e2e/__screenshots__/explorer.spec.ts/explorer-dark.png differ diff --git a/tests/e2e/__screenshots__/explorer.spec.ts/explorer-light.png b/tests/e2e/__screenshots__/explorer.spec.ts/explorer-light.png index 53d56b9..0a5c935 100644 Binary files a/tests/e2e/__screenshots__/explorer.spec.ts/explorer-light.png and b/tests/e2e/__screenshots__/explorer.spec.ts/explorer-light.png differ diff --git a/tests/e2e/__screenshots__/explorer.spec.ts/explorer-phone.png b/tests/e2e/__screenshots__/explorer.spec.ts/explorer-phone.png index 34a0c90..61e9976 100644 Binary files a/tests/e2e/__screenshots__/explorer.spec.ts/explorer-phone.png and b/tests/e2e/__screenshots__/explorer.spec.ts/explorer-phone.png differ diff --git a/tests/e2e/__screenshots__/matrix.spec.ts/matrix-phone.png b/tests/e2e/__screenshots__/matrix.spec.ts/matrix-phone.png index 9dd84a9..929e81e 100644 Binary files a/tests/e2e/__screenshots__/matrix.spec.ts/matrix-phone.png and b/tests/e2e/__screenshots__/matrix.spec.ts/matrix-phone.png differ diff --git a/tests/e2e/__screenshots__/scanner.spec.ts/scanner-dark.png b/tests/e2e/__screenshots__/scanner.spec.ts/scanner-dark.png index 2b44ae3..78856e1 100644 Binary files a/tests/e2e/__screenshots__/scanner.spec.ts/scanner-dark.png and b/tests/e2e/__screenshots__/scanner.spec.ts/scanner-dark.png differ diff --git a/tests/e2e/__screenshots__/scanner.spec.ts/scanner-light.png b/tests/e2e/__screenshots__/scanner.spec.ts/scanner-light.png index 1473e37..444d505 100644 Binary files a/tests/e2e/__screenshots__/scanner.spec.ts/scanner-light.png and b/tests/e2e/__screenshots__/scanner.spec.ts/scanner-light.png differ diff --git a/tests/e2e/__screenshots__/scanner.spec.ts/scanner-phone.png b/tests/e2e/__screenshots__/scanner.spec.ts/scanner-phone.png index 6619ab2..81c371f 100644 Binary files a/tests/e2e/__screenshots__/scanner.spec.ts/scanner-phone.png and b/tests/e2e/__screenshots__/scanner.spec.ts/scanner-phone.png differ