openvibers/openvibe.domains Commits
Agent layer: /.well-known/agent.json and Markdown twins of the public pages (#8)
Changes
nnew file mode 100644@@@ -0,0 +1,17 @@++'use strict';++// What openvibe.domains tells an agent about itself (openvibe-shared/agent-layer): /.well-known/agent.json.++// Every field is something this site really serves: the open JSON API under /api/v1 and the developers page that++// documents it. No OpenAPI document or MCP endpoint is published.++module.exports = {++ id: 'domains',++ name: 'OpenVibe.Domains',++ purpose: 'Price a domain name across every ending and registrar, with real one-, three- and five-year totals and the renewal traps in plain sight, plus public registration history.',++ home: 'https://openvibe.domains',++ site: 'openvibe.domains',++ api: '/api/v1',++ docs: '/developers',++ policy: {++ auth: ['none', 'Network OIDC'],++ rateLimit: '120 API and name-query requests a minute per address; pages are not rate limited',++ },++};@@@ -153,8 +153,25 @@ async function createApp(opts = {}) {});// ── Pages ───────────────────────────────────────────────++ const pages = createPageRoutes(ctx);app.use(panelRoutes.pages);-- app.use(createPageRoutes(ctx));++ // ── Agent layer (openvibe-shared/agent-layer) ───────────++ // The manifest and the Markdown twins of the public pages. Gated to twin requests only, so a browser always++ // reaches the site's own route (and the panel's signed-in pages are never rendered signed out).++ {++ const agentLayer = require('openvibe-shared/agent-layer');++ const agent = express.Router();++ agent.use((req, _res, next) => {++ const twinPath = req.path.endsWith('.md');++ const twinFormat = req.query.format === 'markdown';++ const twinAccept = !!(req.headers.accept && req.accepts(['html', 'text/markdown']) === 'text/markdown');++ if (req.path === '/.well-known/agent.json' || twinPath || twinFormat || twinAccept) return next();++ return next('router');++ });++ agentLayer.mount(agent, { descriptor: require('./agent-descriptor'), render: pages.renderPublicPage });++ app.use(agent);++ }++ app.use(pages);// eslint-disable-next-line no-unused-varsapp.use((err, req, res, _next) => {@@@ -68,7 +68,7 @@ function createDiscoveryRoutes(ctx) {sitemaps: [abs('/sitemap.xml')],allow: ['/'],disallow: [],-- }));++ }) + `# Agent manifest: ${abs('/.well-known/agent.json')}\n`);});r.get('/sitemap.xml', async (_req, res) => {@@@ -10,6 +10,7 @@** Every page is server-rendered, complete without JavaScript and cached for a short public window* (five minutes, two for a name query). The views are placeholders; this file builds the model.++ * renderPublicPage is the one model builder: the HTML routes and the agent layer's Markdown twins both use it.*/const views = require('../views');const { parseName, parseTld } = require('../names');@@@ -28,47 +29,106 @@ function createPageRoutes(ctx) {// The clients never throw; this keeps a misbehaving stand-in from turning a page into a 500.const safe = (fn) => Promise.resolve().then(fn).catch(() => null);-- const send = (res, status, body, cache = PAGE_CACHE) => {-- res.status(status).set('Cache-Control', cache).type('html').send(body);++ const send = (res, status, page, cache = PAGE_CACHE) => {++ res.status(status).set('Cache-Control', cache).type('html');++ if (page && page.canonical) res.set('Link', require('openvibe-shared/agent-layer').linkHeaderTags(page.canonical).header);++ res.send(typeof page === 'string' ? page : page.html);};const notFound = (res) => send(res, 404, views.notFound());++ /** The public model behind a path, for the HTML routes and the agent layer. null means no public page. */++ async function renderPublicPage(rawPath, { query } = {}) {++ const p = String(rawPath || '/').replace(/\/+$/, '') || '/';++ const q = query instanceof URLSearchParams ? query : new URLSearchParams();++++ if (p === '/') {++ const [endings, lists] = await Promise.all([++ safe(() => host.tlds({ sort: 'popular' })),++ safe(() => tools.lists({ limit: 10 })),++ ]);++ return { html: views.home({ endings, lists, canonical: abs('/') }), title: null, canonical: abs('/') };++ }++++ if (p === '/search') {++ const raw = typeof q.get('q') === 'string' ? q.get('q').trim() : '';++ const name = raw ? parseName(raw) : null;++ if (!name) return null;++ const check = await safe(() => host.check(name.ascii));++ const rows = check && Array.isArray(check.prices) ? priceRows(check.prices) : [];++ return {++ html: views.search({ name, check, rows, available: check ? check.status : 'unknown', canonical: abs('/search') }),++ title: `${name.display || name.ascii}: price and availability`,++ canonical: abs('/search'),++ };++ }++++ if (p === '/tld') {++ const endings = await host.tlds({ sort: 'renew' });++ return { html: views.endings({ endings, canonical: abs('/tld') }), title: 'Every ending, cheapest to keep', canonical: abs('/tld') };++ }++++ const tldMatch = p.match(/^\/tld\/([^/]+)$/);++ if (tldMatch) {++ let tld;++ try { tld = parseTld(decodeURIComponent(tldMatch[1])); } catch { tld = null; }++ if (!tld) return null;++ const detail = await safe(() => host.tld(tld));++ if (!detail) return null;++ const priced = { ...detail, prices: priceRows(detail.prices) };++ const name = priced.display || `.${priced.tld}`;++ return {++ html: views.tld({ tld: priced, canonical: abs(`/tld/${encodeURIComponent(tld)}`) }),++ title: `${name} domain prices: every registrar, 1, 3 and 5 years`,++ canonical: abs(`/tld/${encodeURIComponent(tld)}`),++ };++ }++++ if (p === '/developers') {++ return { html: views.developers({ canonical: abs('/developers') }), title: 'The Domains API', canonical: abs('/developers') };++ }++++ const domainMatch = p.match(/^\/d\/([^/]+)$/);++ if (domainMatch) {++ let name;++ try { name = parseName(decodeURIComponent(domainMatch[1])); } catch { name = null; }++ if (!name || !name.tld) return null;++ const [history, check] = await Promise.all([++ safe(() => tools.history(name.ascii)),++ safe(() => host.check(name.ascii)),++ ]);++ const rows = check && Array.isArray(check.prices) ? check.prices.filter((row) => row && row.tld === name.tld) : [];++ const canonical = abs(`/d/${encodeURIComponent(name.ascii)}`);++ return {++ html: views.domain({ name, history, prices: priceRows(rows), status: check ? check.status : null, check, canonical }),++ title: `${name.display || name.ascii}: public record and history`,++ canonical,++ };++ }++++ return null;++ }++r.get('/', async (_req, res) => {-- const [endings, lists] = await Promise.all([-- safe(() => host.tlds({ sort: 'popular' })),-- safe(() => tools.lists({ limit: 10 })),-- ]);-- send(res, 200, views.home({ endings, lists, canonical: abs('/') }));++ send(res, 200, await renderPublicPage('/'));});r.get('/search', async (req, res) => {const q = typeof req.query.q === 'string' ? req.query.q.trim() : '';-- const name = q ? parseName(q) : null;-- if (!name) return res.redirect(302, '/');-- const check = await safe(() => host.check(name.ascii));-- const rows = check && Array.isArray(check.prices) ? priceRows(check.prices) : [];-- return send(res, 200, views.search({-- name, check, rows,-- available: check ? check.status : 'unknown',-- canonical: abs('/search'),-- }), QUERY_CACHE);++ const page = await renderPublicPage('/search', { query: new URLSearchParams({ q }) });++ if (!page) return res.redirect(302, '/');++ return send(res, 200, page, QUERY_CACHE);});r.get('/tld/:tld', async (req, res) => {-- const tld = parseTld(req.params.tld);-- if (!tld) return notFound(res);-- const detail = await safe(() => host.tld(tld));-- if (!detail) return notFound(res);-- const priced = { ...detail, prices: priceRows(detail.prices) };-- return send(res, 200, views.tld({ tld: priced, canonical: abs(`/tld/${encodeURIComponent(tld)}`) }));++ const page = await renderPublicPage(`/tld/${encodeURIComponent(req.params.tld)}`);++ return page ? send(res, 200, page) : notFound(res);});r.get('/tld', async (_req, res) => {-- const endings = await host.tlds({ sort: 'renew' });-- send(res, 200, views.endings({ endings, canonical: abs('/tld') }));++ send(res, 200, await renderPublicPage('/tld'));});-- r.get('/developers', (_req, res) => send(res, 200, views.developers({ canonical: abs('/developers') })));++ r.get('/developers', async (_req, res) => send(res, 200, await renderPublicPage('/developers')));// A person who types "/.dev" gets the ending page. The ending is normalized in one 301.r.get(/^\/\.([^/]+)$/, (req, res) => {@@@ -79,22 +139,14 @@ function createPageRoutes(ctx) {});r.get('/d/:domain', async (req, res) => {-- const name = parseName(req.params.domain);-- if (!name || !name.tld) return notFound(res);-- const [history, check] = await Promise.all([-- safe(() => tools.history(name.ascii)),-- safe(() => host.check(name.ascii)),-- ]);-- const rows = check && Array.isArray(check.prices) ? check.prices.filter((row) => row && row.tld === name.tld) : [];-- return send(res, 200, views.domain({-- name, history, prices: priceRows(rows), status: check ? check.status : null, check,-- canonical: abs(`/d/${encodeURIComponent(name.ascii)}`),-- }), QUERY_CACHE);++ const page = await renderPublicPage(`/d/${encodeURIComponent(req.params.domain)}`);++ return page ? send(res, 200, page, QUERY_CACHE) : notFound(res);});r.use(createDiscoveryRoutes(ctx));r.use((_req, res) => notFound(res));++ r.renderPublicPage = renderPublicPage;return r;}@@@ -60,7 +60,7 @@ const NAV = [];/** The page around a body: the shared shell with the room's stylesheets and behaviour. */--function layout({ title, description, canonical, robots = 'index, follow', body, active = '/', jsonLd = [] }) {++function layout({ title, description, canonical, robots = 'index, follow', body, active = '/', jsonLd = [], agentLayer = false }) {const navbar = {service: 'domains',apiBase: NETWORK,@@@ -83,6 +83,7 @@ function layout({ title, description, canonical, robots = 'index, follow', body,siteName: NAME,description: description || `${ROOM.line}. Every ending and registrar for a name, with what you will really pay over 1, 3 and 5 years.`,canonical,++ agentLayer,robots,jsonLd,home: '/',@@@ -187,7 +188,7 @@ function home({ endings, lists, canonical } = {}) {// the room is one or two screens. Both stay in the page for the crawlers and open where there is room.body = foldSection(body, 'class="sc-faq"', 'faq-fold');body = foldSection(body, 'class="sc-code"', 'dev-fold');-- return layout({ title: null, canonical, body: `<div class="dm-wrap">${body}</div>`, active: '/', jsonLd: [{ '@context': 'https://schema.org', '@type': 'WebSite', name: NAME, url: 'https://openvibe.domains/', potentialAction: { '@type': 'SearchAction', target: 'https://openvibe.domains/search?q={name}', 'query-input': 'required name=name' } }] });++ return layout({ agentLayer: true, title: null, canonical, body: `<div class="dm-wrap">${body}</div>`, active: '/', jsonLd: [{ '@context': 'https://schema.org', '@type': 'WebSite', name: NAME, url: 'https://openvibe.domains/', potentialAction: { '@type': 'SearchAction', target: 'https://openvibe.domains/search?q={name}', 'query-input': 'required name=name' } }] });}/** A table of priced rows: ending or registrar first, then the year totals, sorted by 3 years. */@@@ -224,14 +225,14 @@ ${available === 'registered' && check ? `<p class="dm-act"><a class="dm-btn" hre${rows.length ? `<section aria-labelledby="dm-all-h"><div class="dm-head-row"><h2 id="dm-all-h">${esc(label)} in every ending</h2></div><p class="dm-fine">What each ending costs to register and keep, cheapest over three years first. Availability is checked for <b>${esc(full)}</b>; tap another ending to check it.${latest(rows) ? ` Prices checked ${esc(day(latest(rows)))}.` : ''}</p>${priceTable(rows, { by: 'ending', label: `${label} in every ending`, check: name ? name.label : null })}</section>` : '<p class="dm-fine">No prices for this name\'s endings yet.</p>'}`;-- return layout({ title: `${full}: price and availability`, description: `${full}: whether it is free, and what every ending costs over 1, 3 and 5 years.`, robots: 'noindex, follow', body: `<div class="dm-wrap">${body}</div>`, active: '/' });++ return layout({ agentLayer: true, title: `${full}: price and availability`, description: `${full}: whether it is free, and what every ending costs over 1, 3 and 5 years.`, robots: 'noindex, follow', body: `<div class="dm-wrap">${body}</div>`, active: '/' });}function endings({ endings: list, canonical } = {}) {const rows = priceRows((list && list.tlds) || []);const body = `<header class="dm-page-h"><h1>Every ending</h1><p class="dm-lede">All the endings OpenVibe prices, cheapest to keep over three years first.${latest(rows) ? ` Prices checked ${esc(day(latest(rows)))}.` : ''}</p>${searchBox()}</header>${rows.length ? priceTable(rows, { by: 'ending', label: 'Every ending' }) : kit.empty({ title: 'No prices yet', text: 'The price lists are being read. Try again in a few minutes.' })}`;-- return layout({ title: 'Every ending, cheapest to keep', description: 'Every domain ending OpenVibe prices, sorted by what three years really cost.', canonical, body: `<div class="dm-wrap">${body}</div>`, active: '/tld' });++ return layout({ agentLayer: true, title: 'Every ending, cheapest to keep', description: 'Every domain ending OpenVibe prices, sorted by what three years really cost.', canonical, body: `<div class="dm-wrap">${body}</div>`, active: '/tld' });}/** A small history chart: each registrar's first-year price over time, as steps (real changes only). */@@@ -271,7 +272,7 @@ ${searchBox()}</header><p class="dm-fine">From each registrar's own public price list.${latest(rows) ? ` Checked ${esc(day(latest(rows)))}.` : ''} Affiliate links, where there are any, are marked and never change the order.</p>${rows.length ? priceTable(rows, { by: 'registrar', label: `Registrars for ${name}` }) : '<p class="dm-fine">No registrar prices this ending yet.</p>'}</section><section aria-labelledby="dm-hist-h"><h2 id="dm-hist-h">Price history</h2>${chart || `<p class="dm-fine">No price changes recorded${rows.length && rows[0].first_seen_at ? ` since ${esc(day(rows.map((r) => r.first_seen_at).filter(Boolean).sort()[0]))}` : ''}. Every change from now on is kept here.</p>`}</section>`;-- return layout({ title: `${name} domain prices: every registrar, 1, 3 and 5 years`, description: `What ${name} costs at every registrar OpenVibe reads: first year, renewal and the real 3- and 5-year totals.`, canonical, body: `<div class="dm-wrap">${body}</div>`, active: '/tld' });++ return layout({ agentLayer: true, title: `${name} domain prices: every registrar, 1, 3 and 5 years`, description: `What ${name} costs at every registrar OpenVibe reads: first year, renewal and the real 3- and 5-year totals.`, canonical, body: `<div class="dm-wrap">${body}</div>`, active: '/tld' });}function domain({ name, history, prices = [], status = null, check = null, canonical } = {}) {@@@ -295,7 +296,7 @@ ${status ? `<p class="dm-status ${(STATUS[status] || ['is-unknown'])[0]}"><span<section aria-labelledby="dm-rec-h"><h2 id="dm-rec-h">Public record</h2>${record || `<p class="dm-fine">OpenVibe keeps a domain's history once it has been looked up ${Number(h.min_lookups || 5)} times${h.lookups ? ` (${Number(h.lookups)} so far)` : ''}. <a href="https://whois.openvibe.tools/?q=${encodeURIComponent(name ? name.ascii : '')}">Look it up now on OpenVibe.Tools</a>.</p>`}${timeline ? `<h3>What changed</h3>${timeline}` : ''}</section>${prices.length ? `<section aria-labelledby="dm-p-h"><h2 id="dm-p-h">What ${esc(dot(name.tld))} costs</h2>${priceTable(prices, { by: 'ending', label: `${dot(name.tld)} prices` })}</section>` : ''}`;-- return layout({ title: `${full}: public record and history`, description: `${full}: its registrar, dates and how they changed, from public records only.`, canonical, robots: snaps.length ? 'index, follow' : 'noindex, follow', body: `<div class="dm-wrap">${body}</div>` });++ return layout({ agentLayer: true, title: `${full}: public record and history`, description: `${full}: its registrar, dates and how they changed, from public records only.`, canonical, robots: snaps.length ? 'index, follow' : 'noindex, follow', body: `<div class="dm-wrap">${body}</div>` });}/** The API, for people and agents: every endpoint with a call that copies. */@@@ -310,7 +311,7 @@ function developers({ canonical } = {}) {<section aria-labelledby="dm-ep-h"><h2 id="dm-ep-h">Endpoints</h2><dl class="dm-eps">${rows.map(([ep, what]) => `<div><dt><code>${esc(ep)}</code></dt><dd>${esc(what)}</dd></div>`).join('')}</dl></section>${sc.code({ title: 'Try it', samples: [{ label: 'Price a name', lang: 'bash', code: 'curl "https://openvibe.domains/api/v1/search?q=sushitruck"' }, { label: 'JavaScript', lang: 'js', code: "const r = await fetch('https://openvibe.domains/api/v1/tlds/dev').then((x) => x.json());\nconsole.log(r.tld.prices[0]);" }] })}<p class="dm-fine">Limits: 120 requests a minute from one address. Errors are problem+json (RFC 9457). <a href="/llms.txt">llms.txt</a> describes the site for agents.</p>`;-- return layout({ title: 'The Domains API', description: 'Open JSON for domain prices, availability and history: every ending and registrar, with 1, 3 and 5-year totals.', canonical, body: `<div class="dm-wrap">${body}</div>`, active: '/developers' });++ return layout({ agentLayer: true, title: 'The Domains API', description: 'Open JSON for domain prices, availability and history: every ending and registrar, with 1, 3 and 5-year totals.', canonical, body: `<div class="dm-wrap">${body}</div>`, active: '/developers' });}function notFound() {nnew file mode 100644@@@ -0,0 +1,78 @@++'use strict';++++/** The agent layer: the manifest and the Markdown twins of the public pages, through the same model builder. */++const assert = require('assert');++const { boot, check, done } = require('./helpers');++++const LIST = [++ { tld: 'com', display: '.com', kind: 'generic', currency: 'USD', register: 9.2, renew: 11.08, transfer: 9.2, five_year: 55.4, registrars: 2, checked_at: '2026-10-22T15:16:02.303Z' },++ { tld: 'dev', display: '.dev', kind: 'generic', currency: 'USD', register: 8.75, renew: 12.87, transfer: 12.87, five_year: 60.23, registrars: 2, checked_at: '2026-10-22T15:16:02.303Z' },++];++const PRICES = [++ { registrar: 'Porkbun', registrar_slug: 'porkbun', tld: 'dev', display: '.dev', kind: 'generic', currency: 'USD', register: 8.75, renew: 12.87, transfer: 12.87, five_year: 60.23, checked_at: '2026-10-22T15:16:02.303Z' },++];++const host = {++ async tlds() { return { tlds: LIST, next_cursor: null }; },++ async tld(t) {++ return { tld: t, display: `.${t}`, kind: 'generic', currency: 'USD', prices: PRICES.filter((p) => p.tld === t), history: [] };++ },++ async check(name) {++ return { label: String(name).split('.')[0], name, tld: 'dev', status: 'available', prices: PRICES };++ },++};++const tools = {++ async history(domain) { return { domain, snapshots: [], changes: [], lookups: 0 }; },++ async lists() { return { most_looked_up: [] }; },++};++++(async () => {++ const t = await boot({ host, tools });++ try {++ await check('manifest is JSON with only the endpoints Domains really serves', async () => {++ const r = await t.get('/.well-known/agent.json');++ assert.strictEqual(r.status, 200);++ assert.match(r.headers.get('content-type'), /application\/json/);++ const m = r.json();++ assert.strictEqual(m.id, 'domains');++ assert.strictEqual(m.home, 'https://openvibe.domains');++ assert.strictEqual(m.api, 'https://openvibe.domains/api/v1');++ assert.strictEqual(m.docs, 'https://openvibe.domains/developers');++ assert.deepStrictEqual(m.policy.auth, ['none', 'Network OIDC']);++ assert.strictEqual(m.openapi, undefined);++ assert.strictEqual(m.mcp, undefined);++ });++++ await check('the home names its twin in the head and the Link header', async () => {++ const r = await t.get('/');++ assert.strictEqual(r.status, 200);++ assert.ok(r.text.includes('<link rel="alternate" type="text/markdown" href="https://openvibe.domains/index.md">'));++ assert.match(r.headers.get('link'), /https:\/\/openvibe\.domains\/index\.md>; rel="alternate"; type="text\/markdown"/);++ });++++ await check('/index.md and /tld/dev.md are Markdown of the same pages; unknown twins are 404', async () => {++ const home = await t.get('/index.md');++ assert.strictEqual(home.status, 200);++ assert.match(home.headers.get('content-type'), /text\/markdown/);++ assert.ok(home.text.startsWith('---\ntitle:'));++ assert.ok(home.text.includes('.dev') && home.text.includes('$34.49'));++ const tld = await t.get('/tld/dev.md');++ assert.strictEqual(tld.status, 200);++ assert.ok(tld.text.includes('# .dev domain prices: every registrar, 1, 3 and 5 years'));++ assert.ok(tld.text.includes('Porkbun'));++ assert.strictEqual((await t.get('/no-such-page.md')).status, 404);++ assert.strictEqual((await t.get('/panel.md')).status, 404);++ });++++ await check('/.well-known/security.txt is not swallowed by the layer, and robots names the manifest', async () => {++ const sec = await t.get('/.well-known/security.txt');++ assert.strictEqual(sec.status, 404);++ assert.doesNotMatch(sec.headers.get('content-type') || '', /application\/json/);++ const robots = await t.get('/robots.txt');++ assert.strictEqual(robots.status, 200);++ assert.ok(robots.text.includes('# Agent manifest: https://openvibe.domains/.well-known/agent.json'));++ });++ } finally {++ await t.close();++ done();++ }++})();
new file mode 100644
new file mode 100644
@@ -0,0 +1,17 @@
@@ -0,0 +1,17 @@
+'use strict';
+// What openvibe.domains tells an agent about itself (openvibe-shared/agent-layer): /.well-known/agent.json.
+// Every field is something this site really serves: the open JSON API under /api/v1 and the developers page that
+// documents it. No OpenAPI document or MCP endpoint is published.
+module.exports = {
+ id: 'domains',
+ name: 'OpenVibe.Domains',
+ purpose: 'Price a domain name across every ending and registrar, with real one-, three- and five-year totals and the renewal traps in plain sight, plus public registration history.',
+ home: 'https://openvibe.domains',
+ site: 'openvibe.domains',
+ api: '/api/v1',
+ docs: '/developers',
+ policy: {
+ auth: ['none', 'Network OIDC'],
+ rateLimit: '120 API and name-query requests a minute per address; pages are not rate limited',
+ },
+};
@@ -153,8 +153,25 @@ async function createApp(opts = {}) {
@@ -153,8 +153,25 @@ async function createApp(opts = {}) {
});
});
// ── Pages ───────────────────────────────────────────────
// ── Pages ───────────────────────────────────────────────
+ const pages = createPageRoutes(ctx);
app.use(panelRoutes.pages);
app.use(panelRoutes.pages);
- app.use(createPageRoutes(ctx));
+ // ── Agent layer (openvibe-shared/agent-layer) ───────────
+ // The manifest and the Markdown twins of the public pages. Gated to twin requests only, so a browser always
+ // reaches the site's own route (and the panel's signed-in pages are never rendered signed out).
+ {
+ const agentLayer = require('openvibe-shared/agent-layer');
+ const agent = express.Router();
+ agent.use((req, _res, next) => {
+ const twinPath = req.path.endsWith('.md');
+ const twinFormat = req.query.format === 'markdown';
+ const twinAccept = !!(req.headers.accept && req.accepts(['html', 'text/markdown']) === 'text/markdown');
+ if (req.path === '/.well-known/agent.json' || twinPath || twinFormat || twinAccept) return next();
+ return next('router');
+ });
+ agentLayer.mount(agent, { descriptor: require('./agent-descriptor'), render: pages.renderPublicPage });
+ app.use(agent);
+ }
+ app.use(pages);
// eslint-disable-next-line no-unused-vars
// eslint-disable-next-line no-unused-vars
app.use((err, req, res, _next) => {
app.use((err, req, res, _next) => {
@@ -68,7 +68,7 @@ function createDiscoveryRoutes(ctx) {
@@ -68,7 +68,7 @@ function createDiscoveryRoutes(ctx) {
sitemaps: [abs('/sitemap.xml')],
sitemaps: [abs('/sitemap.xml')],
allow: ['/'],
allow: ['/'],
disallow: [],
disallow: [],
- }));
+ }) + `# Agent manifest: ${abs('/.well-known/agent.json')}\n`);
});
});
r.get('/sitemap.xml', async (_req, res) => {
r.get('/sitemap.xml', async (_req, res) => {
@@ -10,6 +10,7 @@
@@ -10,6 +10,7 @@
*
*
* Every page is server-rendered, complete without JavaScript and cached for a short public window
* Every page is server-rendered, complete without JavaScript and cached for a short public window
* (five minutes, two for a name query). The views are placeholders; this file builds the model.
* (five minutes, two for a name query). The views are placeholders; this file builds the model.
+ * renderPublicPage is the one model builder: the HTML routes and the agent layer's Markdown twins both use it.
*/
*/
const views = require('../views');
const views = require('../views');
const { parseName, parseTld } = require('../names');
const { parseName, parseTld } = require('../names');
@@ -28,47 +29,106 @@ function createPageRoutes(ctx) {
@@ -28,47 +29,106 @@ function createPageRoutes(ctx) {
// The clients never throw; this keeps a misbehaving stand-in from turning a page into a 500.
// The clients never throw; this keeps a misbehaving stand-in from turning a page into a 500.
const safe = (fn) => Promise.resolve().then(fn).catch(() => null);
const safe = (fn) => Promise.resolve().then(fn).catch(() => null);
- const send = (res, status, body, cache = PAGE_CACHE) => {
- res.status(status).set('Cache-Control', cache).type('html').send(body);
+ const send = (res, status, page, cache = PAGE_CACHE) => {
+ res.status(status).set('Cache-Control', cache).type('html');
+ if (page && page.canonical) res.set('Link', require('openvibe-shared/agent-layer').linkHeaderTags(page.canonical).header);
+ res.send(typeof page === 'string' ? page : page.html);
};
};
const notFound = (res) => send(res, 404, views.notFound());
const notFound = (res) => send(res, 404, views.notFound());
+ /** The public model behind a path, for the HTML routes and the agent layer. null means no public page. */
+ async function renderPublicPage(rawPath, { query } = {}) {
+ const p = String(rawPath || '/').replace(/\/+$/, '') || '/';
+ const q = query instanceof URLSearchParams ? query : new URLSearchParams();
+
+ if (p === '/') {
+ const [endings, lists] = await Promise.all([
+ safe(() => host.tlds({ sort: 'popular' })),
+ safe(() => tools.lists({ limit: 10 })),
+ ]);
+ return { html: views.home({ endings, lists, canonical: abs('/') }), title: null, canonical: abs('/') };
+ }
+
+ if (p === '/search') {
+ const raw = typeof q.get('q') === 'string' ? q.get('q').trim() : '';
+ const name = raw ? parseName(raw) : null;
+ if (!name) return null;
+ const check = await safe(() => host.check(name.ascii));
+ const rows = check && Array.isArray(check.prices) ? priceRows(check.prices) : [];
+ return {
+ html: views.search({ name, check, rows, available: check ? check.status : 'unknown', canonical: abs('/search') }),
+ title: `${name.display || name.ascii}: price and availability`,
+ canonical: abs('/search'),
+ };
+ }
+
+ if (p === '/tld') {
+ const endings = await host.tlds({ sort: 'renew' });
+ return { html: views.endings({ endings, canonical: abs('/tld') }), title: 'Every ending, cheapest to keep', canonical: abs('/tld') };
+ }
+
+ const tldMatch = p.match(/^\/tld\/([^/]+)$/);
+ if (tldMatch) {
+ let tld;
+ try { tld = parseTld(decodeURIComponent(tldMatch[1])); } catch { tld = null; }
+ if (!tld) return null;
+ const detail = await safe(() => host.tld(tld));
+ if (!detail) return null;
+ const priced = { ...detail, prices: priceRows(detail.prices) };
+ const name = priced.display || `.${priced.tld}`;
+ return {
+ html: views.tld({ tld: priced, canonical: abs(`/tld/${encodeURIComponent(tld)}`) }),
+ title: `${name} domain prices: every registrar, 1, 3 and 5 years`,
+ canonical: abs(`/tld/${encodeURIComponent(tld)}`),
+ };
+ }
+
+ if (p === '/developers') {
+ return { html: views.developers({ canonical: abs('/developers') }), title: 'The Domains API', canonical: abs('/developers') };
+ }
+
+ const domainMatch = p.match(/^\/d\/([^/]+)$/);
+ if (domainMatch) {
+ let name;
+ try { name = parseName(decodeURIComponent(domainMatch[1])); } catch { name = null; }
+ if (!name || !name.tld) return null;
+ const [history, check] = await Promise.all([
+ safe(() => tools.history(name.ascii)),
+ safe(() => host.check(name.ascii)),
+ ]);
+ const rows = check && Array.isArray(check.prices) ? check.prices.filter((row) => row && row.tld === name.tld) : [];
+ const canonical = abs(`/d/${encodeURIComponent(name.ascii)}`);
+ return {
+ html: views.domain({ name, history, prices: priceRows(rows), status: check ? check.status : null, check, canonical }),
+ title: `${name.display || name.ascii}: public record and history`,
+ canonical,
+ };
+ }
+
+ return null;
+ }
+
r.get('/', async (_req, res) => {
r.get('/', async (_req, res) => {
- const [endings, lists] = await Promise.all([
- safe(() => host.tlds({ sort: 'popular' })),
- safe(() => tools.lists({ limit: 10 })),
- ]);
- send(res, 200, views.home({ endings, lists, canonical: abs('/') }));
+ send(res, 200, await renderPublicPage('/'));
});
});
r.get('/search', async (req, res) => {
r.get('/search', async (req, res) => {
const q = typeof req.query.q === 'string' ? req.query.q.trim() : '';
const q = typeof req.query.q === 'string' ? req.query.q.trim() : '';
- const name = q ? parseName(q) : null;
- if (!name) return res.redirect(302, '/');
- const check = await safe(() => host.check(name.ascii));
- const rows = check && Array.isArray(check.prices) ? priceRows(check.prices) : [];
- return send(res, 200, views.search({
- name, check, rows,
- available: check ? check.status : 'unknown',
- canonical: abs('/search'),
- }), QUERY_CACHE);
+ const page = await renderPublicPage('/search', { query: new URLSearchParams({ q }) });
+ if (!page) return res.redirect(302, '/');
+ return send(res, 200, page, QUERY_CACHE);
});
});
r.get('/tld/:tld', async (req, res) => {
r.get('/tld/:tld', async (req, res) => {
- const tld = parseTld(req.params.tld);
- if (!tld) return notFound(res);
- const detail = await safe(() => host.tld(tld));
- if (!detail) return notFound(res);
- const priced = { ...detail, prices: priceRows(detail.prices) };
- return send(res, 200, views.tld({ tld: priced, canonical: abs(`/tld/${encodeURIComponent(tld)}`) }));
+ const page = await renderPublicPage(`/tld/${encodeURIComponent(req.params.tld)}`);
+ return page ? send(res, 200, page) : notFound(res);
});
});
r.get('/tld', async (_req, res) => {
r.get('/tld', async (_req, res) => {
- const endings = await host.tlds({ sort: 'renew' });
- send(res, 200, views.endings({ endings, canonical: abs('/tld') }));
+ send(res, 200, await renderPublicPage('/tld'));
});
});
- r.get('/developers', (_req, res) => send(res, 200, views.developers({ canonical: abs('/developers') })));
+ r.get('/developers', async (_req, res) => send(res, 200, await renderPublicPage('/developers')));
// A person who types "/.dev" gets the ending page. The ending is normalized in one 301.
// A person who types "/.dev" gets the ending page. The ending is normalized in one 301.
r.get(/^\/\.([^/]+)$/, (req, res) => {
r.get(/^\/\.([^/]+)$/, (req, res) => {
@@ -79,22 +139,14 @@ function createPageRoutes(ctx) {
@@ -79,22 +139,14 @@ function createPageRoutes(ctx) {
});
});
r.get('/d/:domain', async (req, res) => {
r.get('/d/:domain', async (req, res) => {
- const name = parseName(req.params.domain);
- if (!name || !name.tld) return notFound(res);
- const [history, check] = await Promise.all([
- safe(() => tools.history(name.ascii)),
- safe(() => host.check(name.ascii)),
- ]);
- const rows = check && Array.isArray(check.prices) ? check.prices.filter((row) => row && row.tld === name.tld) : [];
- return send(res, 200, views.domain({
- name, history, prices: priceRows(rows), status: check ? check.status : null, check,
- canonical: abs(`/d/${encodeURIComponent(name.ascii)}`),
- }), QUERY_CACHE);
+ const page = await renderPublicPage(`/d/${encodeURIComponent(req.params.domain)}`);
+ return page ? send(res, 200, page, QUERY_CACHE) : notFound(res);
});
});
r.use(createDiscoveryRoutes(ctx));
r.use(createDiscoveryRoutes(ctx));
r.use((_req, res) => notFound(res));
r.use((_req, res) => notFound(res));
+ r.renderPublicPage = renderPublicPage;
return r;
return r;
}
}
@@ -60,7 +60,7 @@ const NAV = [
@@ -60,7 +60,7 @@ const NAV = [
];
];
/** The page around a body: the shared shell with the room's stylesheets and behaviour. */
/** The page around a body: the shared shell with the room's stylesheets and behaviour. */
-function layout({ title, description, canonical, robots = 'index, follow', body, active = '/', jsonLd = [] }) {
+function layout({ title, description, canonical, robots = 'index, follow', body, active = '/', jsonLd = [], agentLayer = false }) {
const navbar = {
const navbar = {
service: 'domains',
service: 'domains',
apiBase: NETWORK,
apiBase: NETWORK,
@@ -83,6 +83,7 @@ function layout({ title, description, canonical, robots = 'index, follow', body,
@@ -83,6 +83,7 @@ function layout({ title, description, canonical, robots = 'index, follow', body,
siteName: NAME,
siteName: NAME,
description: description || `${ROOM.line}. Every ending and registrar for a name, with what you will really pay over 1, 3 and 5 years.`,
description: description || `${ROOM.line}. Every ending and registrar for a name, with what you will really pay over 1, 3 and 5 years.`,
canonical,
canonical,
+ agentLayer,
robots,
robots,
jsonLd,
jsonLd,
home: '/',
home: '/',
@@ -187,7 +188,7 @@ function home({ endings, lists, canonical } = {}) {
@@ -187,7 +188,7 @@ function home({ endings, lists, canonical } = {}) {
// the room is one or two screens. Both stay in the page for the crawlers and open where there is room.
// the room is one or two screens. Both stay in the page for the crawlers and open where there is room.
body = foldSection(body, 'class="sc-faq"', 'faq-fold');
body = foldSection(body, 'class="sc-faq"', 'faq-fold');
body = foldSection(body, 'class="sc-code"', 'dev-fold');
body = foldSection(body, 'class="sc-code"', 'dev-fold');
- return layout({ title: null, canonical, body: `<div class="dm-wrap">${body}</div>`, active: '/', jsonLd: [{ '@context': 'https://schema.org', '@type': 'WebSite', name: NAME, url: 'https://openvibe.domains/', potentialAction: { '@type': 'SearchAction', target: 'https://openvibe.domains/search?q={name}', 'query-input': 'required name=name' } }] });
+ return layout({ agentLayer: true, title: null, canonical, body: `<div class="dm-wrap">${body}</div>`, active: '/', jsonLd: [{ '@context': 'https://schema.org', '@type': 'WebSite', name: NAME, url: 'https://openvibe.domains/', potentialAction: { '@type': 'SearchAction', target: 'https://openvibe.domains/search?q={name}', 'query-input': 'required name=name' } }] });
}
}
/** A table of priced rows: ending or registrar first, then the year totals, sorted by 3 years. */
/** A table of priced rows: ending or registrar first, then the year totals, sorted by 3 years. */
@@ -224,14 +225,14 @@ ${available === 'registered' && check ? `<p class="dm-act"><a class="dm-btn" hre
@@ -224,14 +225,14 @@ ${available === 'registered' && check ? `<p class="dm-act"><a class="dm-btn" hre
${rows.length ? `<section aria-labelledby="dm-all-h"><div class="dm-head-row"><h2 id="dm-all-h">${esc(label)} in every ending</h2></div>
${rows.length ? `<section aria-labelledby="dm-all-h"><div class="dm-head-row"><h2 id="dm-all-h">${esc(label)} in every ending</h2></div>
<p class="dm-fine">What each ending costs to register and keep, cheapest over three years first. Availability is checked for <b>${esc(full)}</b>; tap another ending to check it.${latest(rows) ? ` Prices checked ${esc(day(latest(rows)))}.` : ''}</p>
<p class="dm-fine">What each ending costs to register and keep, cheapest over three years first. Availability is checked for <b>${esc(full)}</b>; tap another ending to check it.${latest(rows) ? ` Prices checked ${esc(day(latest(rows)))}.` : ''}</p>
${priceTable(rows, { by: 'ending', label: `${label} in every ending`, check: name ? name.label : null })}</section>` : '<p class="dm-fine">No prices for this name\'s endings yet.</p>'}`;
${priceTable(rows, { by: 'ending', label: `${label} in every ending`, check: name ? name.label : null })}</section>` : '<p class="dm-fine">No prices for this name\'s endings yet.</p>'}`;
- return layout({ title: `${full}: price and availability`, description: `${full}: whether it is free, and what every ending costs over 1, 3 and 5 years.`, robots: 'noindex, follow', body: `<div class="dm-wrap">${body}</div>`, active: '/' });
+ return layout({ agentLayer: true, title: `${full}: price and availability`, description: `${full}: whether it is free, and what every ending costs over 1, 3 and 5 years.`, robots: 'noindex, follow', body: `<div class="dm-wrap">${body}</div>`, active: '/' });
}
}
function endings({ endings: list, canonical } = {}) {
function endings({ endings: list, canonical } = {}) {
const rows = priceRows((list && list.tlds) || []);
const rows = priceRows((list && list.tlds) || []);
const body = `<header class="dm-page-h"><h1>Every ending</h1><p class="dm-lede">All the endings OpenVibe prices, cheapest to keep over three years first.${latest(rows) ? ` Prices checked ${esc(day(latest(rows)))}.` : ''}</p>${searchBox()}</header>
const body = `<header class="dm-page-h"><h1>Every ending</h1><p class="dm-lede">All the endings OpenVibe prices, cheapest to keep over three years first.${latest(rows) ? ` Prices checked ${esc(day(latest(rows)))}.` : ''}</p>${searchBox()}</header>
${rows.length ? priceTable(rows, { by: 'ending', label: 'Every ending' }) : kit.empty({ title: 'No prices yet', text: 'The price lists are being read. Try again in a few minutes.' })}`;
${rows.length ? priceTable(rows, { by: 'ending', label: 'Every ending' }) : kit.empty({ title: 'No prices yet', text: 'The price lists are being read. Try again in a few minutes.' })}`;
- return layout({ title: 'Every ending, cheapest to keep', description: 'Every domain ending OpenVibe prices, sorted by what three years really cost.', canonical, body: `<div class="dm-wrap">${body}</div>`, active: '/tld' });
+ return layout({ agentLayer: true, title: 'Every ending, cheapest to keep', description: 'Every domain ending OpenVibe prices, sorted by what three years really cost.', canonical, body: `<div class="dm-wrap">${body}</div>`, active: '/tld' });
}
}
/** A small history chart: each registrar's first-year price over time, as steps (real changes only). */
/** A small history chart: each registrar's first-year price over time, as steps (real changes only). */
@@ -271,7 +272,7 @@ ${searchBox()}</header>
@@ -271,7 +272,7 @@ ${searchBox()}</header>
<p class="dm-fine">From each registrar's own public price list.${latest(rows) ? ` Checked ${esc(day(latest(rows)))}.` : ''} Affiliate links, where there are any, are marked and never change the order.</p>
<p class="dm-fine">From each registrar's own public price list.${latest(rows) ? ` Checked ${esc(day(latest(rows)))}.` : ''} Affiliate links, where there are any, are marked and never change the order.</p>
${rows.length ? priceTable(rows, { by: 'registrar', label: `Registrars for ${name}` }) : '<p class="dm-fine">No registrar prices this ending yet.</p>'}</section>
${rows.length ? priceTable(rows, { by: 'registrar', label: `Registrars for ${name}` }) : '<p class="dm-fine">No registrar prices this ending yet.</p>'}</section>
<section aria-labelledby="dm-hist-h"><h2 id="dm-hist-h">Price history</h2>${chart || `<p class="dm-fine">No price changes recorded${rows.length && rows[0].first_seen_at ? ` since ${esc(day(rows.map((r) => r.first_seen_at).filter(Boolean).sort()[0]))}` : ''}. Every change from now on is kept here.</p>`}</section>`;
<section aria-labelledby="dm-hist-h"><h2 id="dm-hist-h">Price history</h2>${chart || `<p class="dm-fine">No price changes recorded${rows.length && rows[0].first_seen_at ? ` since ${esc(day(rows.map((r) => r.first_seen_at).filter(Boolean).sort()[0]))}` : ''}. Every change from now on is kept here.</p>`}</section>`;
- return layout({ title: `${name} domain prices: every registrar, 1, 3 and 5 years`, description: `What ${name} costs at every registrar OpenVibe reads: first year, renewal and the real 3- and 5-year totals.`, canonical, body: `<div class="dm-wrap">${body}</div>`, active: '/tld' });
+ return layout({ agentLayer: true, title: `${name} domain prices: every registrar, 1, 3 and 5 years`, description: `What ${name} costs at every registrar OpenVibe reads: first year, renewal and the real 3- and 5-year totals.`, canonical, body: `<div class="dm-wrap">${body}</div>`, active: '/tld' });
}
}
function domain({ name, history, prices = [], status = null, check = null, canonical } = {}) {
function domain({ name, history, prices = [], status = null, check = null, canonical } = {}) {
@@ -295,7 +296,7 @@ ${status ? `<p class="dm-status ${(STATUS[status] || ['is-unknown'])[0]}"><span
@@ -295,7 +296,7 @@ ${status ? `<p class="dm-status ${(STATUS[status] || ['is-unknown'])[0]}"><span
<section aria-labelledby="dm-rec-h"><h2 id="dm-rec-h">Public record</h2>${record || `<p class="dm-fine">OpenVibe keeps a domain's history once it has been looked up ${Number(h.min_lookups || 5)} times${h.lookups ? ` (${Number(h.lookups)} so far)` : ''}. <a href="https://whois.openvibe.tools/?q=${encodeURIComponent(name ? name.ascii : '')}">Look it up now on OpenVibe.Tools</a>.</p>`}
<section aria-labelledby="dm-rec-h"><h2 id="dm-rec-h">Public record</h2>${record || `<p class="dm-fine">OpenVibe keeps a domain's history once it has been looked up ${Number(h.min_lookups || 5)} times${h.lookups ? ` (${Number(h.lookups)} so far)` : ''}. <a href="https://whois.openvibe.tools/?q=${encodeURIComponent(name ? name.ascii : '')}">Look it up now on OpenVibe.Tools</a>.</p>`}
${timeline ? `<h3>What changed</h3>${timeline}` : ''}</section>
${timeline ? `<h3>What changed</h3>${timeline}` : ''}</section>
${prices.length ? `<section aria-labelledby="dm-p-h"><h2 id="dm-p-h">What ${esc(dot(name.tld))} costs</h2>${priceTable(prices, { by: 'ending', label: `${dot(name.tld)} prices` })}</section>` : ''}`;
${prices.length ? `<section aria-labelledby="dm-p-h"><h2 id="dm-p-h">What ${esc(dot(name.tld))} costs</h2>${priceTable(prices, { by: 'ending', label: `${dot(name.tld)} prices` })}</section>` : ''}`;
- return layout({ title: `${full}: public record and history`, description: `${full}: its registrar, dates and how they changed, from public records only.`, canonical, robots: snaps.length ? 'index, follow' : 'noindex, follow', body: `<div class="dm-wrap">${body}</div>` });
+ return layout({ agentLayer: true, title: `${full}: public record and history`, description: `${full}: its registrar, dates and how they changed, from public records only.`, canonical, robots: snaps.length ? 'index, follow' : 'noindex, follow', body: `<div class="dm-wrap">${body}</div>` });
}
}
/** The API, for people and agents: every endpoint with a call that copies. */
/** The API, for people and agents: every endpoint with a call that copies. */
@@ -310,7 +311,7 @@ function developers({ canonical } = {}) {
@@ -310,7 +311,7 @@ function developers({ canonical } = {}) {
<section aria-labelledby="dm-ep-h"><h2 id="dm-ep-h">Endpoints</h2><dl class="dm-eps">${rows.map(([ep, what]) => `<div><dt><code>${esc(ep)}</code></dt><dd>${esc(what)}</dd></div>`).join('')}</dl></section>
<section aria-labelledby="dm-ep-h"><h2 id="dm-ep-h">Endpoints</h2><dl class="dm-eps">${rows.map(([ep, what]) => `<div><dt><code>${esc(ep)}</code></dt><dd>${esc(what)}</dd></div>`).join('')}</dl></section>
${sc.code({ title: 'Try it', samples: [{ label: 'Price a name', lang: 'bash', code: 'curl "https://openvibe.domains/api/v1/search?q=sushitruck"' }, { label: 'JavaScript', lang: 'js', code: "const r = await fetch('https://openvibe.domains/api/v1/tlds/dev').then((x) => x.json());\nconsole.log(r.tld.prices[0]);" }] })}
${sc.code({ title: 'Try it', samples: [{ label: 'Price a name', lang: 'bash', code: 'curl "https://openvibe.domains/api/v1/search?q=sushitruck"' }, { label: 'JavaScript', lang: 'js', code: "const r = await fetch('https://openvibe.domains/api/v1/tlds/dev').then((x) => x.json());\nconsole.log(r.tld.prices[0]);" }] })}
<p class="dm-fine">Limits: 120 requests a minute from one address. Errors are problem+json (RFC 9457). <a href="/llms.txt">llms.txt</a> describes the site for agents.</p>`;
<p class="dm-fine">Limits: 120 requests a minute from one address. Errors are problem+json (RFC 9457). <a href="/llms.txt">llms.txt</a> describes the site for agents.</p>`;
- return layout({ title: 'The Domains API', description: 'Open JSON for domain prices, availability and history: every ending and registrar, with 1, 3 and 5-year totals.', canonical, body: `<div class="dm-wrap">${body}</div>`, active: '/developers' });
+ return layout({ agentLayer: true, title: 'The Domains API', description: 'Open JSON for domain prices, availability and history: every ending and registrar, with 1, 3 and 5-year totals.', canonical, body: `<div class="dm-wrap">${body}</div>`, active: '/developers' });
}
}
function notFound() {
function notFound() {
new file mode 100644
new file mode 100644
@@ -0,0 +1,78 @@
@@ -0,0 +1,78 @@
+'use strict';
+
+/** The agent layer: the manifest and the Markdown twins of the public pages, through the same model builder. */
+const assert = require('assert');
+const { boot, check, done } = require('./helpers');
+
+const LIST = [
+ { tld: 'com', display: '.com', kind: 'generic', currency: 'USD', register: 9.2, renew: 11.08, transfer: 9.2, five_year: 55.4, registrars: 2, checked_at: '2026-10-22T15:16:02.303Z' },
+ { tld: 'dev', display: '.dev', kind: 'generic', currency: 'USD', register: 8.75, renew: 12.87, transfer: 12.87, five_year: 60.23, registrars: 2, checked_at: '2026-10-22T15:16:02.303Z' },
+];
+const PRICES = [
+ { registrar: 'Porkbun', registrar_slug: 'porkbun', tld: 'dev', display: '.dev', kind: 'generic', currency: 'USD', register: 8.75, renew: 12.87, transfer: 12.87, five_year: 60.23, checked_at: '2026-10-22T15:16:02.303Z' },
+];
+const host = {
+ async tlds() { return { tlds: LIST, next_cursor: null }; },
+ async tld(t) {
+ return { tld: t, display: `.${t}`, kind: 'generic', currency: 'USD', prices: PRICES.filter((p) => p.tld === t), history: [] };
+ },
+ async check(name) {
+ return { label: String(name).split('.')[0], name, tld: 'dev', status: 'available', prices: PRICES };
+ },
+};
+const tools = {
+ async history(domain) { return { domain, snapshots: [], changes: [], lookups: 0 }; },
+ async lists() { return { most_looked_up: [] }; },
+};
+
+(async () => {
+ const t = await boot({ host, tools });
+ try {
+ await check('manifest is JSON with only the endpoints Domains really serves', async () => {
+ const r = await t.get('/.well-known/agent.json');
+ assert.strictEqual(r.status, 200);
+ assert.match(r.headers.get('content-type'), /application\/json/);
+ const m = r.json();
+ assert.strictEqual(m.id, 'domains');
+ assert.strictEqual(m.home, 'https://openvibe.domains');
+ assert.strictEqual(m.api, 'https://openvibe.domains/api/v1');
+ assert.strictEqual(m.docs, 'https://openvibe.domains/developers');
+ assert.deepStrictEqual(m.policy.auth, ['none', 'Network OIDC']);
+ assert.strictEqual(m.openapi, undefined);
+ assert.strictEqual(m.mcp, undefined);
+ });
+
+ await check('the home names its twin in the head and the Link header', async () => {
+ const r = await t.get('/');
+ assert.strictEqual(r.status, 200);
+ assert.ok(r.text.includes('<link rel="alternate" type="text/markdown" href="https://openvibe.domains/index.md">'));
+ assert.match(r.headers.get('link'), /https:\/\/openvibe\.domains\/index\.md>; rel="alternate"; type="text\/markdown"/);
+ });
+
+ await check('/index.md and /tld/dev.md are Markdown of the same pages; unknown twins are 404', async () => {
+ const home = await t.get('/index.md');
+ assert.strictEqual(home.status, 200);
+ assert.match(home.headers.get('content-type'), /text\/markdown/);
+ assert.ok(home.text.startsWith('---\ntitle:'));
+ assert.ok(home.text.includes('.dev') && home.text.includes('$34.49'));
+ const tld = await t.get('/tld/dev.md');
+ assert.strictEqual(tld.status, 200);
+ assert.ok(tld.text.includes('# .dev domain prices: every registrar, 1, 3 and 5 years'));
+ assert.ok(tld.text.includes('Porkbun'));
+ assert.strictEqual((await t.get('/no-such-page.md')).status, 404);
+ assert.strictEqual((await t.get('/panel.md')).status, 404);
+ });
+
+ await check('/.well-known/security.txt is not swallowed by the layer, and robots names the manifest', async () => {
+ const sec = await t.get('/.well-known/security.txt');
+ assert.strictEqual(sec.status, 404);
+ assert.doesNotMatch(sec.headers.get('content-type') || '', /application\/json/);
+ const robots = await t.get('/robots.txt');
+ assert.strictEqual(robots.status, 200);
+ assert.ok(robots.text.includes('# Agent manifest: https://openvibe.domains/.well-known/agent.json'));
+ });
+ } finally {
+ await t.close();
+ done();
+ }
+})();