policy-site #
The policy dashboard served at https://policy.did.bot: surface 2 in
docs/web-surfaces.md. A static page with no backend.
An operator signs in against their own PDS and edits the bot.did.policy and
bot.did.policyBinding records in their own repository. Their servers poll
those records and enforce them.
It is its own package, with its own lockfile, apart from site/.
Commands #
npm ci
npm run dev # http://127.0.0.1:5173, sending the deployed CSP header
npm run build # typechecks, then writes dist/
npm test # every tests/check-*.mjs, against dist/
The checks module is built separately, by scripts/build-policy-wasm.sh,
into public/wasm/. Vite copies it into dist/wasm/ untouched. Without it
the page loads, and says the checks did not load.
scripts/build-policy-anim-wasm.sh builds the background's module beside it.
Without that one the page has a plain ground.
What lives where #
src/oauth-config.ts: the client's OAuth identity. The served/client-metadata.jsonis generated from it by a plugin invite.config.ts, and the page's own scope and redirect come from it.src/copy.ts: every sentence of prose on the page, as⟦…⟧filler until a human writes it.index.htmlnames slots withdata-copy="key", filled at build time.src/checks.ts: the only door to the wasm checks. Every verdict the page shows about a record comes from there.recommended/: the policies the page offers to start from, one file each, with the casesdidbot-pdsjudges them by.src/recommended.tsreads them at build time. See its README.src/session.ts: sign-in and sign-out, and which third parties see what.src/pds.ts: reads of the operator's repository, and its one write,com.atproto.repo.applyWrites.src/xrpc.ts: the HTTP calls behind them. Writes go through the OAuth session'sfetchHandler; every other call carries no credentials.src/servers.ts,src/watch.ts: the operator's servers and whether each enforces the records as they now read.src/directory.ts: the DIDs the typeahead suggests, from those servers only.src/dom.ts: every element is built here, with untrusted text as text nodes.src/routes.ts: every address the page answers, and the flows. Each is one index.html:infra/policy-site/viewer-request.jsrewrites the page's own addresses to it, andserving.mjsruns that same function in the dev and preview servers.src/batch.ts: the write batch, every create, update and delete lined up across both collections, kept insessionStoragefor the session and written as oneapplyWritesguarded by the commit the records were read at.src/flow.tskeeps the operator's place in a flow beside it.src/store.ts: the signed-in state: the records, that commit, the servers, and the batch laid over them. Itsdeploy()is the page's only write.src/views/: one module per view, andwizard.tsfor every flow's steps.main.tsroutes between them.src/ui/policy-syntax.ts: what a policy's JSON means to its engine, for colouring it: kinds, regular expressions, DIDs,⟦…⟧placeholders. Which fields are which comes from the engine's own grammar, whichcrates/didbot-policy-checkexports.src/ui/regex-parts.tscolours regex syntax.src/ui/cedar-tokens.ts: Cedar text cut into coloured parts and into statements, for the editor and the snippets alike.src/ui/cedar-complete.tscompletes Cedar from the schema the server validates it against, whichcrates/didbot-policy-checkexports.src/ui/snippet.ts: read-only JSON, highlighted by walking the parsed value, and Cedar text, highlighted as written. Its JSON text is exactlyJSON.stringify(value, null, 2).src/ui/policy-review.ts: a policy at a glance, as My policies and the recommended pages show it: the record's fields, and the document's body highlighted.src/ui/policy-editor.ts: the policy editor. The record's own fields are form fields, and the engine select sets the document's$type.src/ui/policy-document.tsturns a record into those fields and back, losing nothing.src/ui/document-editor.ts: the document's text editor, CodeMirror, in the engine's language: JSON under its grammar, or Cedar. It completes what the engine allows and shows the checks' problems in place. It runs in a shadow root, because CodeMirror styles a document with a<style>element, which the CSP refuses, and a shadow root with a constructable stylesheet. It and CodeMirror load the first time an editor opens.src/style.css: the palette, as custom properties at the top.src/lattice.ts: the background. It is the landing page's Ising lattice fromcrates/didbot-site-anim, stopped just before the did.bot field switches on, then swept a few times a second. It starts after the checks settle and the page is idle, pauses in a hidden tab, and stays one frame underprefers-reduced-motion.
Against a real server #
scripts/test-policy-e2e.sh starts a didbot-pds to be governed and a
stand-in for the operator's own PDS, serves this build beside them, then runs
tests/e2e-policy.mjs over it in a Chrome the machine already has —
puppeteer-core, which downloads none. With no Chrome the test says which
paths it looked in and skips, and the Rust half deploys instead.
The test signs in on the page as the operator, over TLS, and deploys a
recommended policy through review, bind and confirm.
crates/didbot-swarm/tests/policy_rollout.rs then watches the governed server
enforce it. Last, the page's own wasm computes a digest over the two records,
and it must equal the one the governed server publishes as policy.enforced.
Two surfaces agreeing, rather than one being trusted.
Local sign-in #
Served from 127.0.0.1, the page signs in as the atproto loopback client, with
the same scope, redirected back to itself. localhost redirects to
127.0.0.1 first.
A sign-in is redirected back to / whatever address it started from. The
path to go on to travels in the OAuth library's own state, which it keeps
in its store and hands back on return; src/routes.ts's returnPath
follows it only to one of the page's own addresses.