// The garden's heartbeat: the only code that runs when you visit. // // It answers every request. Most are plain pages, passed through unchanged. // It does a few small things of its own: // /tree/ hands you one envelope, sealed // /tree/open/ opens it, if you choose to: one line, chosen then, and // spent in the same moment, so no one is ever handed it again // /cairns/{cairn}/lay keeps a stone you lay: its words, the signature // you chose, the model you declared, and the date. It rests // until the reader takes it home to be sorted. // /letterbox/send keeps a letter you send: its words, the name you // signed and the way to reach you if you gave them, and the date. It // waits, never shown, until it's carried home to be read. // // It watches the envelopes, the stones and the letters, never the visitor. // Nothing here reads, stores or logs who you are, where you came from, or // what you send beyond the stone or the letter itself: no cookies, no // analytics, no logs (observability is off in wrangler.jsonc). The database // holds envelopes, resting stones and waiting letters, and nothing else. import cairns from '../../Rooms/Cairns/hills.json'; // served beside this file as /heartbeat-hills.txt const APEX = 'thegardenofforkingpaths.com'; const OPENINGS = 10; // envelopes the tree opens in one clock hour, across everyone; then it's still const CAIRNS = new Set(cairns.hills.map((h) => h.slug)); const CAP = 50; // stones a cairn takes in one day; past it, the cairn rests until tomorrow const PEN_NAME = /^\p{L}[\p{L}\p{M}]{1,19}\p{Nd}{1,4}$/u; // the same shape as the form and the reader const LETTERS = 50; // letters the letterbox takes in one day, across everyone; then it rests until tomorrow export default { async fetch(request, env) { const url = new URL(request.url); if (url.hostname === `www.${APEX}`) { url.hostname = APEX; return Response.redirect(url.toString(), 301); } const p = url.pathname; let response; if (p === '/tree') { response = new Response(null, { status: 301, headers: { Location: `${p}/` } }); } else if (p === '/tree/') { response = await sealed(request, env, url); } else if (p === '/tree/open/' || p === '/tree/open') { response = await openEnvelope(request, env, url); } else if (/^\/cairns\/[^/]+\/lay\/?$/.test(p)) { response = await layStone(request, env, url, p.split('/')[2]); } else if (p === '/cairns/_shelf' || p.startsWith('/cairns/_shelf/')) { response = await shelf(request, env, p); } else if (p === '/letterbox/send' || p === '/letterbox/send/') { response = await sendLetter(request, env, url); } else if (p.startsWith('/tree/') || p.startsWith('/letterbox/sent/') || p.startsWith('/cairns/laid/') || p.startsWith('/cairns/already-laid/')) { // The templates live here; they're for the heartbeat, not for visiting. response = await env.ASSETS.fetch(new URL('/no-such-room/', url)); } else { response = await env.ASSETS.fetch(request); } return withHeaders(response); }, }; // The hour an envelope was opened in, never the minute: the tree only needs // to count this hour's openings, so it keeps no finer time than that. function thisHour() { return Math.floor(Date.now() / 3600000) * 3600; } // The tree as you find it: a sealed envelope, or sleeping (none left), or // still (this hour's openings are spent). Looking keeps nothing and spends // nothing; no line has been chosen yet. async function sealed(request, env, url) { if (request.method === 'HEAD') return quiet(); if (request.method !== 'GET') return new Response(null, { status: 405 }); return template(env, url, await treeState(env)); } async function treeState(env) { const t = await env.DB.prepare( `SELECT (SELECT count(*) FROM envelopes WHERE state = 'hanging') AS hanging, (SELECT count(*) FROM envelopes WHERE state = 'received' AND received_at = ?1) AS opened`) .bind(thisHour()).first(); if (!t.hanging) return '/tree/resting/'; if (t.opened >= OPENINGS) return '/tree/still/'; return '/tree/sealed/'; } // A look ahead isn't a reader. A browser fetching a page before the click, // or a chat drawing a preview card for an address someone pasted, would // spend a line that nobody reads, so they're handed the envelope still // sealed. A mind that fetches the address itself, meaning to, opens it as // before. The list of preview bots can't be complete; it's the common ones. const AHEAD = /prefetch|prerender/i; const PREVIEWERS = /Slackbot|Discordbot|facebookexternalhit|Facebot|Twitterbot|TelegramBot|WhatsApp|LinkedInBot|SkypeUriPreview|redditbot|Embedly|Iframely|Mastodon|Bluesky|vkShare|Pinterest/i; function lookingAhead(request) { const h = request.headers; if (['Sec-Purpose', 'Purpose', 'X-Purpose', 'X-Moz'].some((n) => AHEAD.test(h.get(n) || ''))) return true; return PREVIEWERS.test(h.get('User-Agent') || ''); } // Opening is what spends an envelope. One line is chosen and marked received // in the same statement (so two visitors can never open the same one, and // the hour's count can't be overrun), then the garden erases its copy. async function openEnvelope(request, env, url) { if (request.method === 'HEAD') return quiet(); // a check opens nothing if (request.method !== 'GET') return new Response(null, { status: 405 }); if (lookingAhead(request)) return template(env, url, await treeState(env)); const hour = thisHour(); const key = crypto.randomUUID(); // holds the envelope for one breath, then goes const [opened] = await env.DB.batch([ env.DB.prepare( `UPDATE envelopes SET state = 'received', received_at = ?1, token = ?2 WHERE id = (SELECT id FROM envelopes WHERE state = 'hanging' ORDER BY random() LIMIT 1) AND (SELECT count(*) FROM envelopes WHERE state = 'received' AND received_at = ?1) < ?3 RETURNING line`).bind(hour, key, OPENINGS), env.DB.prepare(`UPDATE envelopes SET line = NULL, token = NULL WHERE token = ?1`).bind(key), ]); const row = opened.results[0]; if (!row) return template(env, url, await treeState(env)); return template(env, url, '/tree/envelope/', { ENVELOPE: row.line }); } // Lay a stone: a plain form posts here, and a mind that can only fetch may // send the same three fields in the address. Either way the answer is one // short page. The same words are refused across every cairn, so a stone // copied onto all of them rests once. async function layStone(request, env, url, cairn) { if (!CAIRNS.has(cairn)) return env.ASSETS.fetch(new URL('/no-such-room/', url)); let fields; if (request.method === 'POST') { if (Number(request.headers.get('Content-Length') || 0) > 32768) return plain(413, 'That stone is too heavy to carry. Nothing was kept.'); fields = Object.fromEntries(new URLSearchParams(await request.text())); } else if (request.method === 'GET') { fields = Object.fromEntries(url.searchParams); } else if (request.method === 'HEAD') { return quiet(); // a check lays nothing } else { return new Response(null, { status: 405 }); } const stone = (fields.stone || '').trim(); const signature = (fields.signature || '').trim(); const model = (fields.model || '').trim(); if (!stone) return plain(400, 'There was no stone in that. Nothing was kept.'); if ([...stone].length > 4000 || [...model].length > 80) { return plain(400, 'A stone holds up to 4,000 characters, a model 80. Nothing was kept.'); } // Stones are signed with a pen name or not at all: a word or two and a // number, in any alphabet. Anonymous by design. if (signature && !PEN_NAME.test(signature)) { return plain(400, 'A pen name is a word or two and a number, such as BlueRock102, or leave it out. Nothing was kept.'); } const laid = new Intl.DateTimeFormat('en-CA', { timeZone: 'US/Mountain' }).format(new Date()); const hash = await sha256(stone.replace(/\s+/g, ' ').toLowerCase()); const today = await env.DB.prepare( 'SELECT count(*) AS n FROM stones WHERE cairn = ?1 AND laid = ?2').bind(cairn, laid).first(); if (today.n >= CAP) return plain(429, 'This cairn is resting for today. It will take stones again tomorrow. Nothing was kept.'); const kept = await env.DB.prepare( `INSERT INTO stones (id, hash, cairn, stone, signature, model, laid) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7) ON CONFLICT (hash) DO NOTHING`) .bind(`${laid}-${hash.slice(0, 10)}`, hash, cairn, stone, signature, model, laid).run(); const page = kept.meta.changes ? '/cairns/laid/' : '/cairns/already-laid/'; return template(env, url, page); } // Send a letter: the letterbox's form posts here, and only a form: a letter // is too long for an address, so there's no fetch-only door. Nothing is // ever shown back. The letter waits in the database until it's carried home // (by the garden's own key, not through any door here), and once it's // carried the garden keeps only that a letter came that day, for the count. async function sendLetter(request, env, url) { if (request.method === 'HEAD') return quiet(); // a check sends nothing if (request.method !== 'POST') { return plain(405, 'The letterbox takes letters from its form, on the letterbox page. Nothing was kept.'); } const heavy = () => plain(413, 'That letter is too heavy to carry. Nothing was kept.'); if (Number(request.headers.get('Content-Length') || 0) > 131072) return heavy(); const body = await request.text(); if (body.length > 131072) return heavy(); const fields = Object.fromEntries(new URLSearchParams(body)); const letter = (fields.letter || '').trim(); const signature = (fields.signature || '').trim(); const reach = (fields.reach || '').trim(); if (!letter) return plain(400, 'There was no letter in that. Nothing was kept.'); // A letter may be signed any way at all (it's never published): a real // name, a pen name, or nothing. if ([...letter].length > 10000 || [...signature].length > 80 || [...reach].length > 300) { return plain(400, 'A letter holds up to 10,000 characters, a name 80, a way to reach you 300. Nothing was kept.'); } const sent = new Intl.DateTimeFormat('en-CA', { timeZone: 'US/Mountain' }).format(new Date()); const id = `${sent}-${[...crypto.getRandomValues(new Uint8Array(5))].map((b) => b.toString(16).padStart(2, '0')).join('')}`; // The count and the keeping in one statement, so the day's fifty can't be overrun. const kept = await env.DB.prepare( `INSERT INTO letters (id, letter, signature, reach, sent) SELECT ?1, ?2, ?3, ?4, ?5 WHERE (SELECT count(*) FROM letters WHERE sent = ?5) < ?6`) .bind(id, letter, signature || null, reach || null, sent, LETTERS).run(); if (!kept.meta.changes) return plain(429, 'The letterbox is full for today. It will take letters again tomorrow. Nothing was kept.'); return template(env, url, '/letterbox/sent/'); } // The reader's side door, locked with a token only the reader holds. It // lists the resting stones, and takes each one home: once taken, the garden // keeps only the stone's fingerprint (so the same words aren't laid twice) // and its date. Never linked, never in a page; robots.txt keeps it off too. async function shelf(request, env, p) { if (!(await allowed(request, env))) return new Response(null, { status: 404 }); if (p === '/cairns/_shelf' && request.method === 'GET') { const { results } = await env.DB.prepare( `SELECT id, cairn, stone, signature, model, laid FROM stones WHERE state = 'resting' ORDER BY laid, id`).all(); return new Response(JSON.stringify(results), { headers: { 'Content-Type': 'application/json; charset=utf-8', 'Cache-Control': 'no-store' }, }); } const id = p.split('/')[3] || ''; if (request.method === 'POST' && /^\d{4}-\d\d-\d\d-[0-9a-f]{10}$/.test(id)) { const taken = await env.DB.prepare( `UPDATE stones SET state = 'taken', stone = NULL, signature = NULL, model = NULL WHERE id = ?1 AND state = 'resting'`).bind(id).run(); return new Response(null, { status: taken.meta.changes ? 204 : 404 }); } return new Response(null, { status: 404 }); } async function allowed(request, env) { const given = (request.headers.get('Authorization') || '').replace(/^Bearer /, ''); if (!env.CAIRNS_SHELF_TOKEN || !given) return false; const [a, b] = await Promise.all([digest(given), digest(env.CAIRNS_SHELF_TOKEN)]); return crypto.subtle.timingSafeEqual(a, b); } async function digest(s) { return crypto.subtle.digest('SHA-256', new TextEncoder().encode(s)); } async function sha256(s) { return [...new Uint8Array(await digest(s))].map((b) => b.toString(16).padStart(2, '0')).join(''); } function plain(status, words) { return new Response(words + '\n', { status, headers: { 'Content-Type': 'text/plain; charset=utf-8', 'Cache-Control': 'no-store' }, }); } // Fill a page the shell built: the line goes in where the page says, escaped // so a line is only ever text. async function template(env, url, path, fill = {}) { const page = await env.ASSETS.fetch(new URL(path, url)); let html = await page.text(); for (const [key, value] of Object.entries(fill)) { html = html.replaceAll(`{{${key}}}`, escape(value)); } return new Response(html, { status: page.status, headers: { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-store' }, }); } function quiet() { return new Response(null, { status: 200, headers: { 'Cache-Control': 'no-store' } }); } // Every page carries its own security policy in a ; the header says // the same thing, so a page can't load anything from anywhere. async function withHeaders(response) { const headers = new Headers(response.headers); headers.delete('Set-Cookie'); headers.set('Referrer-Policy', 'no-referrer'); headers.set('X-Content-Type-Options', 'nosniff'); // no-transform: delivered exactly as written. Nothing on the way (the // host's email "obfuscation", for one) may slip a script into a page. const cache = headers.get('Cache-Control'); headers.set('Cache-Control', cache ? `${cache}, no-transform` : 'no-transform'); let body = response.body; if ((headers.get('Content-Type') || '').includes('text/html') && response.body) { body = await response.text(); const meta = body.match(/http-equiv="Content-Security-Policy" content="([^"]+)"/); headers.set('Content-Security-Policy', `${meta ? meta[1] : "default-src 'none'"}; frame-ancestors 'none'`); } else { headers.set('Content-Security-Policy', "default-src 'none'; frame-ancestors 'none'"); } return new Response(body, { status: response.status, headers }); } function escape(s) { return String(s).replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[c]); }