Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -4143,10 +4143,14 @@ harness; see [cal/test/harness.ts](cal/test/harness.ts) and
Hashes rather than a nonce because the staged documents are PRECOMPRESSED
(gotcha 14): nothing can be injected per request into bytes brotli'd at build
time, and the runtime has no brotli encoder to redo them. The live
worker-rendered pages (`/whoareyou`, `/around`, `/coffee`, `/search`, `/ledger`,
worker-rendered pages (`/around`, `/coffee`, `/search`, `/ledger`,
`/rn/admin`, `/serendipity`) are NOT precompressed, so a per-response nonce is
the right mechanism there and is the open follow-up. They keep the loose policy
until then, which is no worse than before.
until then, which is no worse than before. **The other way off that list is
to stop rendering per request**: `/security` (2026-09-16) and `/whoareyou`
(2026-09-25) are built documents now, with their live values fetched after
load. `src/worker/lib/island.ts` is that convention for rows, and its header
says when JSON slots fit better.

Three things verified in a real browser rather than assumed, all on 2026-07-30:
a HASHED `<script type="speculationrules">` is allowed and an unhashed one
Expand Down
4 changes: 2 additions & 2 deletions config/unsafe-html-baseline.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
"src/worker/security.ts": 2,
"src/worker/terminal.ts": 2,
"src/worker/updates.ts": 3,
"src/worker/whoareyou.ts": 3
"src/worker/whoareyou.ts": 2
},
"total": 43
"total": 42
}
7 changes: 7 additions & 0 deletions src/content/md/whoareyou.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,13 @@ are no fixed values to publish here. For the machine-readable form of exactly
what your request revealed, request
<https://aadhar.sh/whoareyou.json>, which returns the same fields as JSON.

The page itself is identical for every visitor and is built once at deploy.
The values arrive in a second request, to
<https://aadhar.sh/whoareyou/values.html>, an HTML fragment the edge renders
for that request alone. So they describe the request that fetched them,
normally the same connection as the page. The referrer is the exception, and
the browser fills that row in from what it sent with the page.

## What the live page shows

- **Network adapter**: the IP that connected, its ASN and network operator, and
Expand Down
28 changes: 26 additions & 2 deletions src/worker/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ import { handleSearch, handleSearchJson } from "./search.ts";
import { handleSecurityJson, renderSecurityCenter } from "./security.ts";
import { handleTool } from "./terminal.ts";
import { handleSystemRestore, handleUpdatesJson, handleWindowsUpdate } from "./updates.ts";
import { handleWhoareyou, handleWhoareyouJson } from "./whoareyou.ts";
import { handleWhoareyouJson, handleWhoareyouValues, renderWhoareyouPage, VALUES_URL as WHOAREYOU_VALUES_URL } from "./whoareyou.ts";
import { handleWritingIndex, handleWritingPost } from "./writing.ts";
import { handleLlmsFull } from "./x402.ts";
import { cronSerendipity, handleSerendipity, withSerendipitySecurityHeaders } from "../../serendipity/serendipity.ts";
Expand Down Expand Up @@ -420,8 +420,9 @@ const ROUTE_TABLE: Array<[path: string, handler: RouteHandler]> = [

["/hit", handleHit],

["/whoareyou", handleWhoareyou],
["/whoareyou", routeWhoareyou],
["/whoareyou.json", handleWhoareyouJson],
[WHOAREYOU_VALUES_URL, handleWhoareyouValues],
["/security", routeSecurity],
["/security.json", handleSecurityJson],
["/reading", handleReading],
Expand Down Expand Up @@ -1076,6 +1077,29 @@ async function routeSecurity(request: SiteRequest, env: Env) {
return live;
}

// /whoareyou is a built document since 2026-09-25, with its per-request values
// as an island from /whoareyou/values.html (whoareyou.ts says why and what moved).
// Same shape as routeSecurity above, down to the twin carrying the page's noindex.
async function routeWhoareyou(request: SiteRequest, env: Env) {
if (wantsMarkdown(request)) {
const md = await serveMarkdownTwin(request, env, "/whoareyou.md", { "x-robots-tag": "noindex" });
if (md) return md;
}
const headers = {
...GENERATED_PAGE_HEADERS,
"x-robots-tag": "noindex",
"referrer-policy": "strict-origin-when-cross-origin",
};
const response = await serveStaticPage(request, env, { headers });
if (response.status !== 404) return response;
// `bun run dev` stages no bake; render live so the page works there. The
// build refuses to ship without the file, so production never takes this arm.
try { await response.body?.cancel(); } catch {}
const live = renderWhoareyouPage();
for (const [k, v] of Object.entries(headers)) live.headers.set(k, v);
return live;
}

function routeLens(request: SiteRequest, env: Env, ctx: ExecutionContext, url: URL) {
// A target-bearing Lens response spends crawler/browser budget and contains
// third-party data, so it remains the live no-store Worker path. The bare,
Expand Down
69 changes: 69 additions & 0 deletions src/worker/lib/island.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// lib/island.ts — the site's one convention for a page that is MOSTLY fixed and
// partly per request.
//
// The page is rendered ONCE by build.ts into the staged tree, so it gets what
// every built document gets: a brotli q11 twin, a dcz delta against the family
// and per-page dictionaries, an ETag, and sha256 CSP hashes for its inline
// scripts. The per-request part is an ISLAND: a placeholder in the built bytes,
// and one same-origin fetch after load that swaps in an HTML fragment the Worker
// renders for that request. Precompressed bytes cannot take a per-request
// injection (the runtime ships no brotli encoder), so this split is what a
// per-request page has to become to earn any of those.
//
// It is the homepage's shape (/photos/grid.html, /rn/tracks.html) and the shape
// Topcoat calls a "shard", written down once so the next page takes it by
// importing three functions rather than by copying a hydrator out of
// index.html. A page whose live part is a handful of SCALARS wants /security's
// lighter form instead (JSON into data-attributes); an island is for rows.
//
// Four rules, each learned on the homepage first:
// 1. The placeholder comes from the SAME renderer as the fragment, fed a
// placeholder model, so the two share markup and the swap moves as little
// as possible.
// 2. The preload carries `crossorigin` even same-origin. Without it the
// preload is mode no-cors and cannot match the script's cors fetch, so the
// fragment is requested twice.
// 3. The script requires the `x-island` marker before injecting. A 5xx from
// the edge is also text/html and also resolves fine.
// 4. A failure leaves the placeholder and says so (data-state="failed"),
// because a page that shows what it read must not show a guess.
import { html, type Html } from "./html.ts";

// Spelled out again inside islandScript, whose text is a constant on purpose; a
// contract test holds the two together.
export const ISLAND_MARKER = "x-island";

/** The fragment response. `no-store` because the body is one request's. */
export function islandResponse(body: Html, headers: Record<string, string> = {}): Response {
return new Response(body.html, {
headers: {
"content-type": "text/html; charset=utf-8",
"cache-control": "no-store, must-revalidate",
"x-content-type-options": "nosniff",
// Worth fetching, worthless in an index: a bare fragment is not a page.
"x-robots-tag": "noindex",
[ISLAND_MARKER]: "1",
...headers,
},
});
}

/** For `<head>`: starts the fragment request while the document is still parsing. */
export function islandPreload(url: string): Html {
return html`<link rel="preload" as="fetch" href="${url}" crossorigin>`;
}

/** The mount. `noscript` is what a reader without scripts gets instead of the swap. */
export function islandMount(id: string, url: string, placeholder: Html, noscript: Html): Html {
return html`<div id="${id}" data-island="${url}" data-state="pending">${placeholder}<noscript>${noscript}</noscript></div>`;
}

/** The one inline script, and it takes no arguments: it finds every mount by
* its `data-island` attribute and reads the URL from there. So the script is
* byte-identical on every page that uses it (one CSP hash, one thing to read),
* and nothing caller-supplied is ever interpolated into script text. On success
* it dispatches a bubbling `island` event, so a page can finish off the rows
* the server structurally cannot fill. */
export function islandScript(): Html {
return html`<script>(function(){if(!window.fetch)return;var ms=document.querySelectorAll("[data-island]");for(var k=0;k<ms.length;k++)(function(m){fetch(m.getAttribute("data-island")).then(function(r){if(!r.ok||r.headers.get("x-island")!=="1")throw 0;return r.text()}).then(function(t){m.innerHTML=t;m.setAttribute("data-state","live");m.dispatchEvent(new Event("island",{bubbles:true}))}).catch(function(){m.setAttribute("data-state","failed")})})(ms[k])})()</script>`;
}
Loading
Loading