diff options
| author | Adam Soltys <[email protected]> | 2026-09-27 19:10:05 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-09-27 19:10:05 -0700 |
| commit | 55cb118f25ddd69ffe9cc4bd44a36b1e689ee19e (patch) | |
| tree | 2446aaf0c3827ac124d9c75d967b62a8ef6d6f5a /names/server.js | |
| parent | 10b7a97a3ecfc2f8e084c81520c78942ac745ec5 (diff) | |
| download | coinosv3-55cb118f25ddd69ffe9cc4bd44a36b1e689ee19e.tar.gz coinosv3-55cb118f25ddd69ffe9cc4bd44a36b1e689ee19e.zip | |
Links in posts show as the cards their pages describe.
A plain link becomes an Open Graph card: picture, title, a line, the site.
The registrar fetches the page on the reader's behalf (names/preview.js,
GET /preview?url=): only public hosts, at most half a megabyte of HTML,
cached a day. The client warms the card with the rest of the post so the row
arrives whole, keeps a few hundred cards across sessions, and leaves a page
with nothing to say as a bare link. Pictures and YouTube keep their boxes.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01VhUKyXKtsJ67QAFfb7QR9D
Diffstat (limited to 'names/server.js')
| -rw-r--r-- | names/server.js | 23 |
1 files changed, 23 insertions, 0 deletions
diff --git a/names/server.js b/names/server.js index d99e330..82d00ec 100644 --- a/names/server.js +++ b/names/server.js @@ -27,6 +27,7 @@ import { HDKey } from '@scure/bip32'; import { mnemonicToSeedSync } from '@scure/bip39'; import { lnBackend } from '../bridge/ln.js'; import { ArkManager } from '../src/ark/manager.js'; +import { fetchPreview } from './preview.js'; import { decodeAddress } from '../src/ark/proto.js'; import { decodeNoffer } from '../src/noffer.js'; import { decodeBolt11 } from '../src/ark/lightning.js'; @@ -847,6 +848,7 @@ setInterval(() => { drainWebhooks().catch(() => {}); }, 30_000); // crude per-IP limiter const rate = new Map(); +const previews = new Map(); // url -> { meta, until } function rateOk(ip, limit = 10) { const now = Date.now(); const arr = (rate.get(ip) || []).filter((t) => t > now - 60_000); @@ -982,6 +984,27 @@ Bun.serve({ return json({ error: 'not found' }, 404); } + // A link in a post, as the card its page describes for itself (Open + // Graph). Cached a day either way: a page with nothing to show is asked + // again only after an hour. Public data; the CDN may cache it too. + if (url.pathname === '/preview' && req.method === 'GET') { + if (!rateOk(ip, 120)) return json({ error: 'rate limited' }, 429); + const target = (url.searchParams.get('url') || '').trim(); + if (!/^https?:\/\/\S{4,2000}$/.test(target)) return json({ error: 'bad url' }, 400); + const now = Date.now(); + let hit = previews.get(target); + if (!hit || now > hit.until) { + const meta = await fetchPreview(target).catch(() => null); + hit = { meta, until: now + (meta ? 24 : 1) * 3600_000 }; + previews.set(target, hit); + if (previews.size > 5000) previews.delete(previews.keys().next().value); + } + return new Response(JSON.stringify(hit.meta || {}), { + headers: { 'content-type': 'application/json', 'access-control-allow-origin': '*', + 'cache-control': 'public, max-age=' + (hit.meta ? 86400 : 3600) }, + }); + } + if (url.pathname === '/health') { return json({ ok: true, domain: DOMAIN, names: Object.keys(state.names).length, |
