aboutsummaryrefslogtreecommitdiffstats
path: root/names/server.js
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-09-27 19:10:05 -0700
committerAdam Soltys <[email protected]>2026-09-27 19:10:05 -0700
commit55cb118f25ddd69ffe9cc4bd44a36b1e689ee19e (patch)
tree2446aaf0c3827ac124d9c75d967b62a8ef6d6f5a /names/server.js
parent10b7a97a3ecfc2f8e084c81520c78942ac745ec5 (diff)
downloadcoinosv3-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.js23
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,