Skip to content

fix(website): render Fig 1's hotspots as Astro markup and escape its JSON blocks - #26

Merged
Hebilicious merged 1 commit into
audit/docs-site-headersfrom
audit/figure-escaping
Oct 14, 2026
Merged

Hebilicious merged 1 commit into
audit/docs-site-headersfrom
audit/figure-escaping

Conversation

@Hebilicious

@Hebilicious Hebilicious commented Oct 13, 2026 •

Copy link
Copy Markdown
Owner

Closes #20

What changed

  • website/src/components/Exchange.astro: the hotspot layer used to be an HTML string with unescaped d, data-hot and aria-label values. The <svg> element and its hotspot paths are now Astro markup, which escapes every attribute. Only the kit's art, which is generated from build-time constants, still goes through set:html.
  • website/src/json-script.ts (new): jsonForScript escapes <, >, &, U+2028 and U+2029, and both application/json data blocks now use it.
  • The built page is the same except that <path/> becomes <path></path>.
  • This layer holds only the escaping. The turnstile text and the search-pigeonhole changes to the same component are in audit/docs-claims.

How it was reproduced

Not reproduced. This was found by reading the code, and it cannot be reached:

  • every value the figure interpolates is a build-time constant;
  • the site has no dynamic route and reads nothing from the URL into the figure.

So no scenario fails on the parent layer (audit/docs-site-headers). The change is defence in depth, and website:build, website:figure-audit, website:headers-check and website:links-check pass on this layer.

Refs #21 (security audit tracking issue). Part of stack #42; review bottom-up, each layer shows only its own diff.

@changeset-bot

changeset-bot Bot commented Oct 13, 2026 •

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 3df9646

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@Hebilicious
Hebilicious added this pull request to stack #37 October 13, 2026 20:10
@Hebilicious
Hebilicious force-pushed the audit/figure-escaping branch from c3d505c to b685261 Compare October 14, 2026 06:04
@Hebilicious
Hebilicious removed this pull request from stack #37 October 14, 2026 06:05
@Hebilicious
Hebilicious added this pull request to stack #42 October 14, 2026 06:05
@Hebilicious Hebilicious changed the title Render Fig 1's hotspots as Astro markup and escape its JSON blocks fix(website): render Fig 1's hotspots as Astro markup and escape its JSON blocks Oct 14, 2026
@Hebilicious
Hebilicious force-pushed the audit/figure-escaping branch from b685261 to 9fca7d8 Compare October 14, 2026 06:10
@Hebilicious
Hebilicious force-pushed the audit/figure-escaping branch from 9fca7d8 to ed7b76c Compare October 14, 2026 07:09
…JSON blocks

The hotspot layer was an HTML string with unescaped attribute values, and the
two data blocks were raw JSON.stringify output written through set:html. The
SVG element and its hotspots are now Astro markup, which escapes every
attribute, and the data blocks escape <, >, &, U+2028 and U+2029 through
website/src/json-script.ts. The built page is unchanged apart from
<path></path> in place of <path/>.

Refs #20
@Hebilicious
Hebilicious force-pushed the audit/figure-escaping branch from ed7b76c to 3df9646 Compare October 14, 2026 08:50
@Hebilicious
Hebilicious merged commit 28b17a8 into main Oct 14, 2026
5 of 6 checks passed
@Hebilicious
Hebilicious deleted the audit/figure-escaping branch October 14, 2026 08:56
Hebilicious added a commit that referenced this pull request Oct 14, 2026
… changes them (#50)

Closes #49

## What changed

- **`.github/workflows/check.yml`:** adds a `deploy` job. It runs after
`check` passes, on a push to `main` or a manual run. It asks moon which
tasks the push affects (`moon query affected` from `github.event.before`
to the pushed commit), then deploys:
  - the **demo** when `infra:demo-deploy` is affected;
- the **docs** when `website:build` or `infra:docs-deploy` is affected,
building the site first;
- nothing when neither is affected. A manual run, or a push with no
usable base, deploys both.

The job uses the `cloudflare` environment and runs one deploy at a time
without cancelling one in progress. It fails with a clear error if a
credential is missing.
- **`infra/moon.yml`:** `infra:demo-deploy` declares the runtime code
the demo's Workers are built from as its `inputs`, and
`infra:docs-deploy` declares its stack files. These lists are the only
path rules: the workflow keeps none of its own.
- **`AGENTS.md`, `docs/deploy.md`:** say that CI redeploys the
maintainer's stages, and which token permissions it needs.

Two traps the workflow avoids, both checked locally:
- With `CI=true`, moon silently skips a `runInCI: "skip"` task, so the
deploy steps run the same `alchemy deploy … --stage demo|docs --yes`
commands directly. The tasks keep `runInCI: "skip"`, so a stray `moon
ci` never deploys.
- Without a terminal, `moon query affected` waits to read changed files
from stdin and hangs, so the workflow closes stdin (`< /dev/null`).

## How it was reproduced

Not a bug. To check the path rules, I replayed the job's decision with
this branch's moon inputs over the last 16 merges to `main`:

```
demo docs 31f1281 fix(market-worker): limit the Pearl Abyss rate per region, not for the whole deployment (#48
-    -    8988949 docs(agris): name an issue in every pull request, reproduce before fixing, and free the demo
-    docs 76c2457 docs: correct the search, limit and namespace claims, and Fig 1's search pigeonhole (#36)
demo docs f76218d fix(gateway-worker): serve GraphiQL under a strict CSP, and send nosniff and no referrer eve
demo docs aaabc17 feat(infra)!: limit each client of a self-hosted gateway to 60 requests a minute by default 
demo docs af0118d fix(gateway-worker): admit each client's market reads exactly, and every request through the
demo docs 92720e0 fix(market-worker): hold one Pearl Abyss rate allowance for the whole deployment (#32)
demo docs 98d50ea fix(market-worker): bound the Pearl Abyss coordinator's queue and let one caller's abort end
demo docs 5672fee fix(market-subgraph)!: hold Market.items to the 100-item batch cap and require Market.catalo
demo docs 3677cbb fix(market-worker): report Pearl Abyss throttling as 429 and budget refusals as QUERY_BUDGET
demo docs 3dd2aee fix: mask market and catalog errors to a closed set, and keep originals out of the gateway's
demo docs 139323a chore: add a security policy, a minimum release age and a pinned, checksummed proto installe
-    docs 28b17a8 fix(website): render Fig 1's hotspots as Astro markup and escape its JSON blocks (#26)
-    docs 526a3b9 feat(website): send security headers from the docs site (#25)
demo docs aa2f4a5 fix(pearl-abyss): cap the Huffman decoder's declared output at 8 MiB (#41)
-    -    ab3e94f docs(agris): fast-forward the main checkout to origin/main after every merge (#46)
```

Website-only merges (#25, #26, #36) deploy only the docs. Process-only
merges (#43, #46) deploy nothing. Runtime merges deploy both, because
the site renders runtime facts (cache windows, limits) through
`@group(agris-sources)`.

This PR changes neither stage's inputs, so merging it deploys nothing.
The deploy itself is first exercised by a manual run once
`CLOUDFLARE_API_TOKEN` is set (see below).

## Credentials

- **Done:** the `cloudflare` environment exists, only `main` may deploy
to it, and it holds `CLOUDFLARE_ACCOUNT_ID`.
- **Missing:** `CLOUDFLARE_API_TOKEN`. The local OAuth sign-ins (Alchemy
and Wrangler) are refused by Cloudflare's token endpoints (`9109
Unauthorized`), and Alchemy's own token command needs the account's
Global API Key. The token has to be created in the dashboard with
Workers Scripts edit, Account Settings read and Secrets Store edit.

## Checks

- [x] `moon run agris:check` passes on this branch
- [ ] In a stack, this pull request holds one concern and its own layer
passes `agris:check` (not a stack)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Figure builds HTML with unescaped interpolation (latent, not reachable today)

1 participant