diff --git a/README.md b/README.md index 52b5bdb..636c7e5 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,18 @@ `lexdns` is a tiny web app that resolves ATProto Lexicon schemas from NSIDs. +## Usage + +```sh +# For metadata, including the ATURI: +curl https://lex.desertthunder.dev/api/resolve/app.bsky.feed.post + +# For just the schema: +curl https://lex.desertthunder.dev/api/resolve/com.atproto.repo.getRecord | jq '.lexicon' +``` + +### How it Works + 1. Validate the requested NSID. 2. Convert its authority to a DNS name and query TXT over DNS over HTTPS. 3. Read a `did=...` TXT value from `_lexicon.`. diff --git a/public/og.png b/public/og.png new file mode 100644 index 0000000000000000000000000000000000000000..86ec883e300a9206f37e4276c8eddf30576a1ef8 GIT binary patch literal 26942 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 literal 0 HcmV?d00001 diff --git a/public/styles/components/shell.css b/public/styles/components/shell.css index 129ac58..43afaa3 100644 --- a/public/styles/components/shell.css +++ b/public/styles/components/shell.css @@ -138,12 +138,32 @@ border-radius: var(--radius-sm); } +.curl-example__bar { + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--space-3); +} + .curl-example span { color: var(--muted); font-size: var(--size-sm); font-weight: 600; } +.curl-example button { + padding: 0.45rem 0.7rem; + color: var(--text); + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius-sm); + cursor: pointer; +} + +.curl-example button:hover { + filter: brightness(0.97); +} + .curl-example code { white-space: nowrap; } diff --git a/src/main.ts b/src/main.ts index 84aa585..84707ec 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,6 +1,7 @@ import { Hono } from 'hono'; import { apiError, jsonResponse } from './http'; import { mapResolveError, resolveLexicon, resolvePathNsid } from './lexicon'; +import type { ResolvedLexicon, ResolvedLexiconResponse } from './types'; import { Index } from './view'; import { JSONView } from './view/partials'; @@ -21,7 +22,7 @@ app.get('/partials/resolve', async (c) => { const nsid = c.req.query('nsid')?.trim() ?? ''; try { const resolved = await resolveLexicon(nsid, c.env); - return c.html(JSONView(resolved, nsid)); + return c.html(JSONView(withLinks(resolved, new URL(c.req.url).origin), nsid)); } catch (error) { const mapped = mapResolveError(error); return c.html(JSONView({ error: { code: mapped.code, message: mapped.message } }, nsid)); @@ -34,9 +35,10 @@ app.options('/api/resolve/*', () => { app.get('/api/resolve/*', async (c) => { try { - const nsid = resolvePathNsid(new URL(c.req.url).pathname); + const url = new URL(c.req.url); + const nsid = resolvePathNsid(url.pathname); const resolved = await resolveLexicon(nsid, c.env); - return jsonResponse(resolved, { headers: CORS_HEADERS }); + return jsonResponse(withLinks(resolved, url.origin), { headers: CORS_HEADERS }); } catch (error) { const mapped = mapResolveError(error); return jsonResponse( @@ -50,5 +52,15 @@ app.notFound(() => { return apiError(404, 'not_found', 'Not found'); }); +export function withLinks(resolved: ResolvedLexicon, origin: string): ResolvedLexiconResponse { + return { + ...resolved, + _links: { + self: { href: `${origin}/api/resolve/${encodeURIComponent(resolved.nsid)}` }, + source: { href: resolved.source.url } + } + }; +} + export { app }; export default app; diff --git a/src/types.ts b/src/types.ts index 59a309a..b51611f 100644 --- a/src/types.ts +++ b/src/types.ts @@ -23,6 +23,15 @@ export type ResolvedLexicon = { cache: 'hit' | 'miss'; }; +export type ResolvedLexiconLinks = { + self: { href: string }; + source: { href: string }; +}; + +export type ResolvedLexiconResponse = ResolvedLexicon & { + _links: ResolvedLexiconLinks; +}; + export type DidDocument = { service?: Array<{ id?: string; type?: string; serviceEndpoint?: string }> }; export type RepoGetRecordResponse = { value?: unknown }; diff --git a/src/view/index.tsx b/src/view/index.tsx index 4a51879..8e31ff8 100644 --- a/src/view/index.tsx +++ b/src/view/index.tsx @@ -5,7 +5,7 @@ import { Resolver } from './partials/form'; export function Index({ baseUrl }: { baseUrl: string }) { return ( - + -

- curl - curl {baseUrl}/api/resolve/com.example.foo +
+
+ Usage + +
+ {usageCommand}
); diff --git a/src/view/partials/helmet.tsx b/src/view/partials/helmet.tsx index 41963b4..32c4c39 100644 --- a/src/view/partials/helmet.tsx +++ b/src/view/partials/helmet.tsx @@ -1,6 +1,9 @@ const DESC = 'Resolve ATProto Lexicon schemas from NSIDs through DNS TXT discovery.'; const TITLE = 'lexdns | Lexicon REST API'; -export function Helmet({ canonicalUrl }: { canonicalUrl: string }) { +const CANONICAL_URL = 'https://lex.desertthunder.dev/'; +const OG_IMAGE_URL = 'https://lex.desertthunder.dev/og.png'; + +export function Helmet() { return ( @@ -11,12 +14,17 @@ export function Helmet({ canonicalUrl }: { canonicalUrl: string }) { - - + + + + + + + {TITLE} - + diff --git a/test/main.test.ts b/test/main.test.ts new file mode 100644 index 0000000..c5efab7 --- /dev/null +++ b/test/main.test.ts @@ -0,0 +1,25 @@ +import { describe, expect, it } from 'vitest'; +import { withLinks } from '../src/main'; +import type { ResolvedLexicon } from '../src/types'; + +function resolvedLexicon(nsid = 'com.example.foo'): ResolvedLexicon { + return { + nsid, + hash: 'sha256:test', + fetchedAt: '2026-06-18T00:00:00.000Z', + source: { name: '_lexicon.example.com', url: `at://did:plc:example/com.atproto.lexicon.schema/${nsid}` }, + lexicon: { lexicon: 1, id: nsid, defs: {} }, + cache: 'miss' + }; +} + +describe('API response presenter', () => { + it('adds HAL-style links to resolved lexicons', () => { + expect(withLinks(resolvedLexicon(), 'https://lexdns.example')).toMatchObject({ + _links: { + self: { href: 'https://lexdns.example/api/resolve/com.example.foo' }, + source: { href: 'at://did:plc:example/com.atproto.lexicon.schema/com.example.foo' } + } + }); + }); +}); diff --git a/test/view.test.ts b/test/view.test.ts index f0b8448..de26276 100644 --- a/test/view.test.ts +++ b/test/view.test.ts @@ -1,6 +1,8 @@ import { describe, expect, it } from 'vitest'; import { renderToString } from 'hono/jsx/dom/server'; import { JSONView } from '../src/view/partials'; +import { Resolver } from '../src/view/partials/form'; +import { Helmet } from '../src/view/partials/helmet'; describe('View helpers', () => { it('escapes JSON before rendering the Prism partial', () => { @@ -28,4 +30,22 @@ describe('View helpers', () => { expect(html).toContain('Error: com.example'); }); + it('renders a copyable usage command', () => { + const html = renderToString(Resolver({ baseUrl: 'https://lexdns.example' })); + + expect(html).toContain('Usage'); + expect(html).toContain('curl https://lexdns.example/api/resolve/site.standard.publication | jq '.lexicon''); + expect(html).toContain('x-ref="usage"'); + expect(html).toContain('copyUsage()'); + }); + + it('renders canonical and social preview metadata', () => { + const html = renderToString(Helmet()); + + expect(html).toContain('rel="canonical" href="https://lex.desertthunder.dev/"'); + expect(html).toContain('property="og:url" content="https://lex.desertthunder.dev/"'); + expect(html).toContain('property="og:image" content="https://lex.desertthunder.dev/og.png"'); + expect(html).toContain('name="twitter:card" content="summary_large_image"'); + expect(html).toContain('name="twitter:image" content="https://lex.desertthunder.dev/og.png"'); + }); });