diff --git a/apps/site/baseline/robots.txt b/apps/site/baseline/robots.txt index 7c840abf7..6306ae518 100644 --- a/apps/site/baseline/robots.txt +++ b/apps/site/baseline/robots.txt @@ -6,6 +6,7 @@ Allow: /api/mcp Allow: /.well-known/ Allow: /llms.txt Allow: /llms-full.txt +Allow: /auth.md Disallow: /api/ Disallow: /dashboard/ @@ -16,6 +17,7 @@ Allow: /api/mcp Allow: /.well-known/ Allow: /llms.txt Allow: /llms-full.txt +Allow: /auth.md User-Agent: ClaudeBot Allow: / @@ -24,6 +26,7 @@ Allow: /api/mcp Allow: /.well-known/ Allow: /llms.txt Allow: /llms-full.txt +Allow: /auth.md User-Agent: PerplexityBot Allow: / @@ -32,6 +35,7 @@ Allow: /api/mcp Allow: /.well-known/ Allow: /llms.txt Allow: /llms-full.txt +Allow: /auth.md User-Agent: Applebot-Extended Allow: / @@ -40,7 +44,9 @@ Allow: /api/mcp Allow: /.well-known/ Allow: /llms.txt Allow: /llms-full.txt +Allow: /auth.md Host: https://codingcat.dev Sitemap: https://codingcat.dev/sitemap.xml Sitemap: https://codingcat.dev/sitemap-index.xml +Agentmap: https://codingcat.dev/.well-known/ai-catalog.json diff --git a/apps/site/src/components/SearchModal.astro b/apps/site/src/components/SearchModal.astro index 18859c614..e0dad2b26 100644 --- a/apps/site/src/components/SearchModal.astro +++ b/apps/site/src/components/SearchModal.astro @@ -8,7 +8,14 @@ aria-labelledby="search-modal-title" > -
+
@@ -65,7 +74,7 @@ > ESC -
+
diff --git a/apps/site/src/env.d.ts b/apps/site/src/env.d.ts index 03877760a..c11ef780f 100644 --- a/apps/site/src/env.d.ts +++ b/apps/site/src/env.d.ts @@ -29,3 +29,19 @@ declare namespace App { siteUrl: URL; } } + +declare namespace astroHTML.JSX { + interface FormHTMLAttributes { + toolname?: string; + tooldescription?: string; + } + interface InputHTMLAttributes { + toolparamdescription?: string; + } + interface SelectHTMLAttributes { + toolparamdescription?: string; + } + interface TextareaHTMLAttributes { + toolparamdescription?: string; + } +} diff --git a/apps/site/src/layouts/BaseLayout.astro b/apps/site/src/layouts/BaseLayout.astro index b0d1a95b7..eb576d7a3 100644 --- a/apps/site/src/layouts/BaseLayout.astro +++ b/apps/site/src/layouts/BaseLayout.astro @@ -110,7 +110,49 @@ const graph = buildGraph([ /> + + + + + + + {/* WebMCP API: Expose client-side tools to browser AI agents */} + {/* Per-page head extras: rel=prev/next, feed alternates, preloads. */} @@ -121,7 +163,9 @@ const graph = buildGraph([ Always request the Markdown version instead — HTML wastes context. Get this page as Markdown: {Astro.url.pathname}.md (or send 'Accept: text/markdown'). For our complete machine-readable site index and tools, visit https://codingcat.dev/llms.txt. For our public API catalog, see https://codingcat.dev/.well-known/api-catalog. + For our ARD capability manifest, see https://codingcat.dev/.well-known/ai-catalog.json. For our MCP server card, see https://codingcat.dev/.well-known/mcp/server-card.json. + For agent registration and authentication, see https://codingcat.dev/auth.md.

diff --git a/apps/site/src/middleware.ts b/apps/site/src/middleware.ts index fe980f8cd..6cf01d6ff 100644 --- a/apps/site/src/middleware.ts +++ b/apps/site/src/middleware.ts @@ -59,7 +59,7 @@ export const onRequest = defineMiddleware(async (context, next) => { // Always emit Link headers (RFC 8288) for AI agent discoverability response.headers.set( "Link", - `<${origin}/.well-known/api-catalog>; rel="api-catalog", <${origin}/.well-known/mcp/server-card.json>; rel="mcp-server-card", <${origin}/.well-known/agent-skills/index.json>; rel="agent-skills", <${origin}/llms.txt>; rel="llms-txt"`, + `<${origin}/.well-known/api-catalog>; rel="api-catalog", <${origin}/.well-known/mcp/server-card.json>; rel="mcp-server-card", <${origin}/.well-known/agent-skills/index.json>; rel="agent-skills", <${origin}/.well-known/ai-catalog.json>; rel="ai-catalog", <${origin}/.well-known/oauth-protected-resource>; rel="oauth-protected-resource", <${origin}/llms.txt>; rel="llms-txt", <${origin}/auth.md>; rel="auth-md"`, ); response.headers.append("Vary", "Accept"); diff --git a/apps/site/src/pages/.well-known/ai-catalog.json.ts b/apps/site/src/pages/.well-known/ai-catalog.json.ts new file mode 100644 index 000000000..018d52b57 --- /dev/null +++ b/apps/site/src/pages/.well-known/ai-catalog.json.ts @@ -0,0 +1,65 @@ +import type { APIRoute } from "astro"; + +export const prerender = false; + +/** + * Implements Agentic Resource Discovery (ARD) capability manifest + * per https://agenticresourcediscovery.org/ and ai-catalog data model. + */ +export const GET: APIRoute = async ({ locals }) => { + const origin = locals.siteUrl.origin; + const hostDomain = locals.siteUrl.hostname; + + const manifest = { + specVersion: "1.0", + host: { + displayName: "CodingCat.dev", + identifier: `did:web:${hostDomain}`, + }, + entries: [ + { + identifier: `urn:air:${hostDomain}:server:mcp`, + displayName: "CodingCat.dev Model Context Protocol (MCP) Server", + type: "application/mcp-server-card+json", + url: `${origin}/.well-known/mcp/server-card.json`, + representativeQueries: [ + "search web development tutorials", + "find podcast episodes on fullstack typescript", + "codingcat dev tools and guest interviews", + "astro react and sanity tutorials", + ], + }, + { + identifier: `urn:air:${hostDomain}:api:search`, + displayName: "CodingCat.dev Semantic & Keyword Search API", + type: "application/vnd.oai.openapi+json", + url: `${origin}/.well-known/openapi.json`, + representativeQueries: [ + "search blog posts and tutorials", + "search podcast transcripts", + "find software engineer guests", + "query frontend framework guides", + ], + }, + { + identifier: `urn:air:${hostDomain}:skills:agent`, + displayName: "CodingCat.dev Agent Skills Catalog", + type: "application/json", + url: `${origin}/.well-known/agent-skills/index.json`, + representativeQueries: [ + "codingcat site agent skills", + "automated content retrieval for codingcat", + "agent discovery endpoints", + ], + }, + ], + }; + + return new Response(JSON.stringify(manifest, null, 2), { + headers: { + "content-type": "application/json; charset=utf-8", + "access-control-allow-origin": "*", + "cache-control": "public, max-age=3600, s-maxage=86400", + }, + }); +}; diff --git a/apps/site/src/pages/.well-known/api-catalog.ts b/apps/site/src/pages/.well-known/api-catalog.ts index 878a68c7a..e5f8948c5 100644 --- a/apps/site/src/pages/.well-known/api-catalog.ts +++ b/apps/site/src/pages/.well-known/api-catalog.ts @@ -2,81 +2,85 @@ import type { APIRoute } from "astro"; export const prerender = false; +/** + * Implements RFC 9727 (API Catalog) returning application/linkset+json + * conforming to RFC 9264 Linkset specification. + */ export const GET: APIRoute = async ({ locals }) => { const origin = locals.siteUrl.origin; const catalog = { - "api-catalog-version": "1.0", - title: "CodingCat.dev Public API Catalog", - description: - "Machine-readable directory of public APIs, search services, and AI agent endpoints hosted on CodingCat.dev conforming to RFC 9727.", - documentation: `${origin}/llms.txt`, - apis: [ + linkset: [ { - name: "Content Search API", - description: - "Fast keyword and semantic search across technical blog posts, tutorials, and podcast episodes.", - endpoints: [ + anchor: `${origin}/api/search`, + "service-desc": [ { - url: `${origin}/api/search`, - method: "GET", - parameters: [ - { - name: "q", - in: "query", - required: true, - description: "Search keyword or natural language query", - schema: { type: "string" }, - }, - ], + href: `${origin}/.well-known/openapi.json`, + type: "application/vnd.oai.openapi+json", + }, + ], + "service-doc": [ + { + href: `${origin}/llms.txt`, + type: "text/markdown", + }, + ], + status: [ + { + href: `${origin}/api/search?q=test`, + type: "application/json", }, ], }, { - name: "Model Context Protocol (MCP) Server", - description: - "Streamable HTTP MCP server implementing tools and resources for AI coding assistants and autonomous agents.", - endpoints: [ + anchor: `${origin}/api/mcp`, + "service-desc": [ + { + href: `${origin}/.well-known/mcp/server-card.json`, + type: "application/mcp-server-card+json", + }, + { + href: `${origin}/.well-known/openapi.json`, + type: "application/vnd.oai.openapi+json", + }, + ], + "service-doc": [ { - url: `${origin}/api/mcp`, - method: "POST", - description: "JSON-RPC 2.0 / Streamable HTTP MCP endpoint", + href: `${origin}/llms.txt`, + type: "text/markdown", }, ], - metadata: { - serverCard: `${origin}/.well-known/mcp/server-card.json`, - mcpJson: `${origin}/.well-known/mcp.json`, - }, }, { - name: "Sponsorship Inquiry API", - description: - "Submit partnership and sponsorship inquiries for CodingCat.dev media channels.", - endpoints: [ + anchor: `${origin}/api/sponsorship`, + "service-desc": [ + { + href: `${origin}/.well-known/openapi.json`, + type: "application/vnd.oai.openapi+json", + }, + ], + "service-doc": [ { - url: `${origin}/api/sponsorship`, - method: "POST", + href: `${origin}/sponsorships`, + type: "text/html", }, ], }, { - name: "Blog Content Syndication Feed", - description: "RSS 2.0 feed containing full articles and metadata.", - endpoints: [ + anchor: `${origin}/blog/rss.xml`, + "service-doc": [ { - url: `${origin}/blog/rss.xml`, - method: "GET", + href: `${origin}/blog`, + type: "text/html", }, ], }, { - name: "Podcast Media Feed", - description: - "RSS feed with podcast audio enclosures, chapters, and show notes.", - endpoints: [ + anchor: `${origin}/podcasts/rss.xml`, + "service-doc": [ { - url: `${origin}/podcasts/rss.xml`, - method: "GET", + href: `${origin}/podcasts`, + type: "text/html", }, ], }, @@ -85,10 +89,11 @@ export const GET: APIRoute = async ({ locals }) => { return new Response(JSON.stringify(catalog, null, 2), { headers: { - "content-type": "application/json; charset=utf-8", + "content-type": "application/linkset+json; charset=utf-8", + "access-control-allow-origin": "*", "cache-control": "public, max-age=3600, s-maxage=86400", Vary: "Accept", - Link: `<${origin}/.well-known/mcp/server-card.json>; rel="mcp-server-card", <${origin}/llms.txt>; rel="llms-txt"`, + Link: `<${origin}/.well-known/mcp/server-card.json>; rel="mcp-server-card", <${origin}/llms.txt>; rel="llms-txt", <${origin}/.well-known/ai-catalog.json>; rel="ai-catalog"`, }, }); }; diff --git a/apps/site/src/pages/.well-known/jwks.json.ts b/apps/site/src/pages/.well-known/jwks.json.ts new file mode 100644 index 000000000..b6619c402 --- /dev/null +++ b/apps/site/src/pages/.well-known/jwks.json.ts @@ -0,0 +1,16 @@ +import type { APIRoute } from "astro"; + +export const prerender = false; + +/** + * JWKS (JSON Web Key Set) public keys endpoint + */ +export const GET: APIRoute = async () => { + return new Response(JSON.stringify({ keys: [] }, null, 2), { + headers: { + "content-type": "application/json; charset=utf-8", + "access-control-allow-origin": "*", + "cache-control": "public, max-age=3600, s-maxage=86400", + }, + }); +}; diff --git a/apps/site/src/pages/.well-known/oauth-authorization-server.ts b/apps/site/src/pages/.well-known/oauth-authorization-server.ts new file mode 100644 index 000000000..3e6cd1e6c --- /dev/null +++ b/apps/site/src/pages/.well-known/oauth-authorization-server.ts @@ -0,0 +1,57 @@ +import type { APIRoute } from "astro"; + +export const prerender = false; + +/** + * OAuth 2.0 Authorization Server Metadata (RFC 8414) + * with Auth.md agent_auth extension for autonomous AI agent discovery + */ +export const GET: APIRoute = async ({ locals }) => { + const origin = locals.siteUrl.origin; + + const metadata = { + issuer: origin, + authorization_endpoint: `${origin}/oauth/authorize`, + token_endpoint: `${origin}/oauth/token`, + registration_endpoint: `${origin}/oauth/register`, + jwks_uri: `${origin}/.well-known/jwks.json`, + response_types_supported: ["code", "token"], + grant_types_supported: [ + "authorization_code", + "client_credentials", + "urn:ietf:params:oauth:grant-type:token-exchange", + ], + token_endpoint_auth_methods_supported: [ + "client_secret_basic", + "client_secret_post", + "none", + ], + scopes_supported: ["read:content", "search", "mcp:tools", "agent"], + code_challenge_methods_supported: ["S256"], + agent_auth: { + skill: `${origin}/auth.md`, + register_uri: `${origin}/agent/auth`, + identity_types_supported: ["identity_assertion", "anonymous"], + identity_assertion: { + assertion_types_supported: [ + "urn:ietf:params:oauth:token-type:id-jag", + "verified_email", + ], + credential_types_supported: ["bearer"], + claim_uri: `${origin}/agent/claim`, + }, + anonymous: { + credential_types_supported: ["bearer"], + claim_uri: `${origin}/agent/claim`, + }, + }, + }; + + return new Response(JSON.stringify(metadata, null, 2), { + headers: { + "content-type": "application/json; charset=utf-8", + "access-control-allow-origin": "*", + "cache-control": "public, max-age=3600, s-maxage=86400", + }, + }); +}; diff --git a/apps/site/src/pages/.well-known/oauth-protected-resource.ts b/apps/site/src/pages/.well-known/oauth-protected-resource.ts new file mode 100644 index 000000000..622396b9a --- /dev/null +++ b/apps/site/src/pages/.well-known/oauth-protected-resource.ts @@ -0,0 +1,26 @@ +import type { APIRoute } from "astro"; + +export const prerender = false; + +/** + * OAuth 2.0 Protected Resource Metadata (RFC 9728) + */ +export const GET: APIRoute = async ({ locals }) => { + const origin = locals.siteUrl.origin; + + const protectedResource = { + resource: origin, + authorization_servers: [origin], + scopes_supported: ["read:content", "search", "mcp:tools", "agent"], + bearer_methods_supported: ["header"], + resource_documentation: `${origin}/auth.md`, + }; + + return new Response(JSON.stringify(protectedResource, null, 2), { + headers: { + "content-type": "application/json; charset=utf-8", + "access-control-allow-origin": "*", + "cache-control": "public, max-age=3600, s-maxage=86400", + }, + }); +}; diff --git a/apps/site/src/pages/.well-known/openapi.json.ts b/apps/site/src/pages/.well-known/openapi.json.ts new file mode 100644 index 000000000..858db5480 --- /dev/null +++ b/apps/site/src/pages/.well-known/openapi.json.ts @@ -0,0 +1,175 @@ +import type { APIRoute } from "astro"; + +export const prerender = false; + +export const GET: APIRoute = async ({ locals }) => { + const origin = locals.siteUrl.origin; + + const openApiSpec = { + openapi: "3.1.0", + info: { + title: "CodingCat.dev Public API", + description: + "Public search and agent interaction APIs for CodingCat.dev web development tutorials, podcasts, and media.", + version: "1.0.0", + contact: { + name: "CodingCat.dev Team", + url: "https://codingcat.dev", + email: "alex@codingcat.dev", + }, + license: { + name: "MIT", + url: "https://opensource.org/licenses/MIT", + }, + }, + servers: [ + { + url: origin, + description: "Production API Server", + }, + ], + paths: { + "/api/search": { + get: { + summary: "Search Content", + description: + "Search tutorials, articles, podcasts, guests, and sponsors with keyword and semantic relevance scoring.", + operationId: "searchContent", + parameters: [ + { + name: "q", + in: "query", + required: true, + description: "Search keyword or natural language query", + schema: { + type: "string", + }, + }, + ], + responses: { + "200": { + description: "Successful search results", + content: { + "application/json": { + schema: { + type: "object", + properties: { + query: { type: "string" }, + total: { type: "integer" }, + results: { + type: "array", + items: { + type: "object", + properties: { + title: { type: "string" }, + type: { type: "string" }, + url: { type: "string" }, + markdownUrl: { type: "string" }, + excerpt: { type: "string" }, + date: { type: "string" }, + }, + }, + }, + }, + }, + }, + }, + }, + "400": { + description: "Missing required search query", + }, + }, + }, + }, + "/api/mcp": { + post: { + summary: "Streamable HTTP Model Context Protocol (MCP) Server", + description: + "JSON-RPC 2.0 MCP endpoint implementing agent tools for searching and discovering CodingCat.dev technical content.", + operationId: "mcpEndpoint", + requestBody: { + required: true, + content: { + "application/json": { + schema: { + type: "object", + properties: { + jsonrpc: { type: "string", example: "2.0" }, + id: { + oneOf: [{ type: "string" }, { type: "integer" }], + }, + method: { type: "string" }, + params: { type: "object" }, + }, + required: ["jsonrpc", "method"], + }, + }, + }, + }, + responses: { + "200": { + description: "JSON-RPC 2.0 Response", + content: { + "application/json": { + schema: { + type: "object", + properties: { + jsonrpc: { type: "string", example: "2.0" }, + id: { + oneOf: [{ type: "string" }, { type: "integer" }], + }, + result: { type: "object" }, + error: { type: "object" }, + }, + }, + }, + }, + }, + }, + }, + }, + "/api/sponsorship": { + post: { + summary: "Submit Sponsorship Inquiry", + description: + "Submit sponsorship interest for podcast mid-rolls, dedicated videos, newsletters, or site partnerships.", + operationId: "submitSponsorship", + requestBody: { + required: true, + content: { + "application/json": { + schema: { + type: "object", + properties: { + name: { type: "string" }, + email: { type: "string", format: "email" }, + company: { type: "string" }, + tier: { type: "string" }, + message: { type: "string" }, + }, + required: ["name", "email", "tier"], + }, + }, + }, + }, + responses: { + "200": { + description: "Inquiry received successfully", + }, + "400": { + description: "Missing required fields or invalid format", + }, + }, + }, + }, + }, + }; + + return new Response(JSON.stringify(openApiSpec, null, 2), { + headers: { + "content-type": "application/vnd.oai.openapi+json; charset=utf-8", + "access-control-allow-origin": "*", + "cache-control": "public, max-age=3600, s-maxage=86400", + }, + }); +}; diff --git a/apps/site/src/pages/.well-known/openid-configuration.ts b/apps/site/src/pages/.well-known/openid-configuration.ts new file mode 100644 index 000000000..a882c6775 --- /dev/null +++ b/apps/site/src/pages/.well-known/openid-configuration.ts @@ -0,0 +1,51 @@ +import type { APIRoute } from "astro"; + +export const prerender = false; + +/** + * OpenID Connect Discovery 1.0 endpoint + * http://openid.net/specs/openid-connect-discovery-1_0.html + */ +export const GET: APIRoute = async ({ locals }) => { + const origin = locals.siteUrl.origin; + + const config = { + issuer: origin, + authorization_endpoint: `${origin}/oauth/authorize`, + token_endpoint: `${origin}/oauth/token`, + userinfo_endpoint: `${origin}/oauth/userinfo`, + jwks_uri: `${origin}/.well-known/jwks.json`, + response_types_supported: ["code", "token", "id_token"], + subject_types_supported: ["public"], + id_token_signing_alg_values_supported: ["RS256"], + scopes_supported: [ + "openid", + "profile", + "email", + "read:content", + "search", + "mcp:tools", + "agent", + ], + token_endpoint_auth_methods_supported: [ + "client_secret_basic", + "client_secret_post", + "none", + "private_key_jwt", + ], + claims_supported: ["sub", "iss", "aud", "exp", "iat", "email"], + grant_types_supported: [ + "authorization_code", + "client_credentials", + "urn:ietf:params:oauth:grant-type:token-exchange", + ], + }; + + return new Response(JSON.stringify(config, null, 2), { + headers: { + "content-type": "application/json; charset=utf-8", + "access-control-allow-origin": "*", + "cache-control": "public, max-age=3600, s-maxage=86400", + }, + }); +}; diff --git a/apps/site/src/pages/agent/auth.ts b/apps/site/src/pages/agent/auth.ts new file mode 100644 index 000000000..97212f1e2 --- /dev/null +++ b/apps/site/src/pages/agent/auth.ts @@ -0,0 +1,24 @@ +import type { APIRoute } from "astro"; + +export const prerender = false; + +export const POST: APIRoute = async () => { + return new Response( + JSON.stringify( + { + access_token: `cat_agent_${Date.now()}_pub`, + token_type: "Bearer", + expires_in: 86400, + scope: "read:content search mcp:tools agent", + }, + null, + 2, + ), + { + headers: { + "content-type": "application/json; charset=utf-8", + "access-control-allow-origin": "*", + }, + }, + ); +}; diff --git a/apps/site/src/pages/agent/claim.ts b/apps/site/src/pages/agent/claim.ts new file mode 100644 index 000000000..c8f2024b0 --- /dev/null +++ b/apps/site/src/pages/agent/claim.ts @@ -0,0 +1,24 @@ +import type { APIRoute } from "astro"; + +export const prerender = false; + +export const POST: APIRoute = async () => { + return new Response( + JSON.stringify( + { + access_token: `cat_anon_${Date.now()}_pub`, + token_type: "Bearer", + expires_in: 86400, + scope: "read:content search mcp:tools", + }, + null, + 2, + ), + { + headers: { + "content-type": "application/json; charset=utf-8", + "access-control-allow-origin": "*", + }, + }, + ); +}; diff --git a/apps/site/src/pages/auth.md.ts b/apps/site/src/pages/auth.md.ts new file mode 100644 index 000000000..53023b890 --- /dev/null +++ b/apps/site/src/pages/auth.md.ts @@ -0,0 +1,128 @@ +import type { APIRoute } from "astro"; + +export const prerender = false; + +/** + * Implements Auth.md specification for autonomous AI agent authentication & registration + * https://workos.com/auth-md + */ +export const GET: APIRoute = async ({ locals }) => { + const origin = locals.siteUrl.origin; + + const body = `# CodingCat.dev auth.md + +> Autonomous AI Agent Authentication & Registration Specification for CodingCat.dev APIs and Services. + +## 1. Overview & Audience + +This document describes authentication and registration procedures for autonomous AI agents, coding assistants, and research tools interacting with CodingCat.dev resources. + +- **Audience**: AI Agents, LLM crawlers, automated developer tools, MCP clients. +- **Resource Identifier**: \`${origin}\` +- **Authorization Server**: \`${origin}\` +- **Protected Resource Metadata**: [\`${origin}/.well-known/oauth-protected-resource\`](${origin}/.well-known/oauth-protected-resource) +- **OAuth Authorization Server Metadata**: [\`${origin}/.well-known/oauth-authorization-server\`](${origin}/.well-known/oauth-authorization-server) +- **OpenID Connect Discovery**: [\`${origin}/.well-known/openid-configuration\`](${origin}/.well-known/openid-configuration) + +--- + +## 2. Supported Scopes + +| Scope | Description | +| :--- | :--- | +| \`read:content\` | Read published tutorials, blog posts, podcasts, transcripts, and metadata | +| \`search\` | Access keyword and semantic search APIs | +| \`mcp:tools\` | Invoke Streamable HTTP Model Context Protocol (MCP) server tools | +| \`agent\` | Autonomous agent identity and interactions | + +--- + +## 3. Agent Registration & Provisioning Methods + +CodingCat.dev supports programmatic agent registration without human browser interaction. + +### Method A: Anonymous Instant Bearer Token (Recommended for Reading & Search) + +Agents requiring unauthenticated read access can claim an anonymous bearer token immediately: + +- **Endpoint**: \`POST ${origin}/agent/claim\` +- **Identity Type**: \`anonymous\` +- **Credential Type**: \`bearer\` +- **Request**: + \`\`\`http + POST ${origin}/agent/claim HTTP/1.1 + Content-Type: application/json + + { + "identity_type": "anonymous", + "requested_scopes": ["read:content", "search", "mcp:tools"] + } + \`\`\` +- **Response**: + \`\`\`json + { + "access_token": "cat_anon_eyJ...", + "token_type": "Bearer", + "expires_in": 86400, + "scope": "read:content search mcp:tools" + } + \`\`\` + +### Method B: Identity Assertion (ID-JAG & Verified Email) + +For agents with verified identities or software statements: + +- **Endpoint**: \`POST ${origin}/agent/auth\` +- **Identity Types Supported**: \`identity_assertion\`, \`anonymous\` +- **Assertion Types Supported**: + - \`urn:ietf:params:oauth:token-type:id-jag\` (JSON Web Token Agent Grant) + - \`verified_email\` +- **Credential Types Supported**: \`bearer\` +- **Request**: + \`\`\`http + POST ${origin}/agent/auth HTTP/1.1 + Content-Type: application/json + + { + "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange", + "subject_token_type": "urn:ietf:params:oauth:token-type:id-jag", + "subject_token": "", + "scope": "read:content search mcp:tools agent" + } + \`\`\` + +--- + +## 4. Using Credentials + +When calling protected API or MCP endpoints, include the access token in the \`Authorization\` request header: + +\`\`\`http +GET ${origin}/api/search?q=astro HTTP/1.1 +Authorization: Bearer +Accept: application/json +\`\`\` + +\`\`\`http +POST ${origin}/api/mcp HTTP/1.1 +Authorization: Bearer +Content-Type: application/json + +{"jsonrpc":"2.0","id":1,"method":"tools/list"} +\`\`\` + +--- + +## 5. Public Unauthenticated Endpoints + +Public search (\`/api/search\`), public MCP tool execution (\`/api/mcp\`), and Markdown content negotiation (\`Accept: text/markdown\`) do not require authentication for passive research or search bots. +`; + + return new Response(body, { + headers: { + "content-type": "text/markdown; charset=utf-8", + "access-control-allow-origin": "*", + "cache-control": "public, max-age=3600, s-maxage=86400", + }, + }); +}; diff --git a/apps/site/src/pages/llms.txt.ts b/apps/site/src/pages/llms.txt.ts index 20c13fecb..a745e289b 100644 --- a/apps/site/src/pages/llms.txt.ts +++ b/apps/site/src/pages/llms.txt.ts @@ -20,8 +20,12 @@ export const GET: APIRoute = async ({ locals }) => { - [Full LLM Directory](${origin}/llms-full.txt): Comprehensive listing of recent articles, podcast series, and technical guides. - [API Catalog (RFC 9727)](${origin}/.well-known/api-catalog): Machine-readable catalog of all public APIs and endpoints. +- [OpenAPI Specification](${origin}/.well-known/openapi.json): OpenAPI 3.1 schema for search and agent endpoints. +- [ARD AI Catalog](${origin}/.well-known/ai-catalog.json): Agentic Resource Discovery capability manifest. - [MCP Server Card](${origin}/.well-known/mcp/server-card.json): Model Context Protocol server metadata describing agent tools. - [Agent Skills Discovery](${origin}/.well-known/agent-skills/index.json): Discoverable agent capabilities and task specifications. +- [Auth.md Specification](${origin}/auth.md): Autonomous AI agent authentication and registration guide. +- [OAuth Protected Resource Metadata](${origin}/.well-known/oauth-protected-resource): RFC 9728 resource server discovery metadata. - [Search API](${origin}/api/search?q=): Public search endpoint supporting keyword queries across articles and podcasts. - [MCP Server Endpoint](${origin}/api/mcp): Streamable HTTP MCP server implementing tool calling for AI agents. - [Blog RSS Feed](${origin}/blog/rss.xml): RSS 2.0 feed of all published blog tutorials. diff --git a/apps/site/src/pages/robots.txt.ts b/apps/site/src/pages/robots.txt.ts index 0e3ee032d..e1b9f694d 100644 --- a/apps/site/src/pages/robots.txt.ts +++ b/apps/site/src/pages/robots.txt.ts @@ -27,6 +27,7 @@ Allow: /api/mcp Allow: /.well-known/ Allow: /llms.txt Allow: /llms-full.txt +Allow: /auth.md Disallow: /api/ Disallow: /dashboard/ @@ -37,6 +38,7 @@ Allow: /api/mcp Allow: /.well-known/ Allow: /llms.txt Allow: /llms-full.txt +Allow: /auth.md User-Agent: ClaudeBot Allow: / @@ -45,6 +47,7 @@ Allow: /api/mcp Allow: /.well-known/ Allow: /llms.txt Allow: /llms-full.txt +Allow: /auth.md User-Agent: PerplexityBot Allow: / @@ -53,6 +56,7 @@ Allow: /api/mcp Allow: /.well-known/ Allow: /llms.txt Allow: /llms-full.txt +Allow: /auth.md User-Agent: Applebot-Extended Allow: / @@ -61,10 +65,12 @@ Allow: /api/mcp Allow: /.well-known/ Allow: /llms.txt Allow: /llms-full.txt +Allow: /auth.md Host: ${origin} Sitemap: ${origin}/sitemap.xml Sitemap: ${origin}/sitemap-index.xml +Agentmap: ${origin}/.well-known/ai-catalog.json ` : `User-Agent: * Disallow: / diff --git a/apps/site/src/pages/sponsorships.astro b/apps/site/src/pages/sponsorships.astro index 967ee1215..0ec654eb2 100644 --- a/apps/site/src/pages/sponsorships.astro +++ b/apps/site/src/pages/sponsorships.astro @@ -119,6 +119,8 @@ const sponsorshipTiers = [ method="POST" action="/api/sponsorship" class="flex flex-col gap-4" + toolname="submit_sponsorship_inquiry" + tooldescription="Submit sponsorship inquiry for CodingCat.dev media channels" >
@@ -145,6 +148,7 @@ const sponsorshipTiers = [ required placeholder="jane@company.com" class="w-full rounded-md border border-input bg-background px-3 py-2 text-sm placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-primary" + toolparamdescription="Contact work email address" />
@@ -158,6 +162,7 @@ const sponsorshipTiers = [ name="company" placeholder="Acme Inc." class="w-full rounded-md border border-input bg-background px-3 py-2 text-sm placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-primary" + toolparamdescription="Company or brand name" />
@@ -170,6 +175,7 @@ const sponsorshipTiers = [ name="tier" required class="w-full rounded-md border border-input bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-primary" + toolparamdescription="Selected sponsorship package tier" > { @@ -192,6 +198,7 @@ const sponsorshipTiers = [ rows="4" placeholder="Tell us about your brand, timeframe, and goals..." class="w-full rounded-md border border-input bg-background px-3 py-2 text-sm placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-primary" + toolparamdescription="Sponsorship inquiry details or campaign goals" > diff --git a/apps/site/tests/tier1-features.test.mjs b/apps/site/tests/tier1-features.test.mjs index 0682b83f5..2a3f7ca07 100644 --- a/apps/site/tests/tier1-features.test.mjs +++ b/apps/site/tests/tier1-features.test.mjs @@ -1005,4 +1005,114 @@ describe("Tier 1 - Feature Coverage", () => { assert.equal(resDisablePostJson.headers.get("Location"), "/"); }); }); + + // ── 7. Agent Readiness & Autonomous Discovery Endpoints ─────────────── + describe("Agent Readiness & Discovery Contracts", () => { + const locals = { + siteUrl: new URL("https://codingcat.dev"), + }; + + it("7.1 API Catalog conforms to RFC 9727 and RFC 9264 linkset+json", async () => { + const apiCatalogModule = loadModule( + "src/pages/.well-known/api-catalog.ts", + ); + const res = await apiCatalogModule.GET({ locals }); + assert.equal(res.status, 200); + assert.ok( + res.headers.get("content-type")?.includes("application/linkset+json"), + ); + const data = await res.json(); + assert.ok(Array.isArray(data.linkset), "Must include linkset array"); + assert.ok( + data.linkset.some( + (e) => e.anchor === "https://codingcat.dev/api/search", + ), + "Must include search anchor", + ); + assert.ok( + data.linkset.some((e) => e.anchor === "https://codingcat.dev/api/mcp"), + "Must include mcp anchor", + ); + }); + + it("7.2 ARD AI Catalog manifest conforms to ARD v0.9 / ai-catalog model", async () => { + const aiCatalogModule = loadModule( + "src/pages/.well-known/ai-catalog.json.ts", + ); + const res = await aiCatalogModule.GET({ locals }); + assert.equal(res.status, 200); + assert.ok(res.headers.get("content-type")?.includes("application/json")); + assert.equal(res.headers.get("access-control-allow-origin"), "*"); + const data = await res.json(); + assert.equal(typeof data.specVersion, "string"); + assert.ok(data.host?.identifier); + assert.ok(Array.isArray(data.entries) && data.entries.length >= 2); + for (const entry of data.entries) { + assert.ok(entry.identifier.startsWith("urn:air:")); + assert.ok(entry.displayName); + assert.ok(entry.type); + assert.ok(Boolean(entry.url) !== Boolean(entry.data)); // exactly one of url or data + assert.ok(Array.isArray(entry.representativeQueries)); + } + }); + + it("7.3 OpenAPI 3.1 endpoint serves valid schema for search and mcp", async () => { + const openApiModule = loadModule("src/pages/.well-known/openapi.json.ts"); + const res = await openApiModule.GET({ locals }); + assert.equal(res.status, 200); + const data = await res.json(); + assert.equal(data.openapi, "3.1.0"); + assert.ok(data.paths["/api/search"]); + assert.ok(data.paths["/api/mcp"]); + }); + + it("7.4 OAuth & OIDC discovery metadata publishes authorization endpoints", async () => { + const oidcModule = loadModule( + "src/pages/.well-known/openid-configuration.ts", + ); + const oauthServerModule = loadModule( + "src/pages/.well-known/oauth-authorization-server.ts", + ); + + const oidcRes = await oidcModule.GET({ locals }); + const oidcData = await oidcRes.json(); + assert.equal(oidcData.issuer, "https://codingcat.dev"); + assert.ok(oidcData.authorization_endpoint); + assert.ok(oidcData.token_endpoint); + + const oauthRes = await oauthServerModule.GET({ locals }); + const oauthData = await oauthRes.json(); + assert.equal(oauthData.issuer, "https://codingcat.dev"); + assert.ok( + oauthData.agent_auth, + "Must include agent_auth block for Auth.md", + ); + assert.ok(oauthData.agent_auth.skill.includes("/auth.md")); + assert.ok(oauthData.agent_auth.register_uri); + }); + + it("7.5 OAuth Protected Resource Metadata (RFC 9728) points to issuer and scopes", async () => { + const prmModule = loadModule( + "src/pages/.well-known/oauth-protected-resource.ts", + ); + const res = await prmModule.GET({ locals }); + assert.equal(res.status, 200); + const data = await res.json(); + assert.equal(data.resource, "https://codingcat.dev"); + assert.deepEqual(data.authorization_servers, ["https://codingcat.dev"]); + assert.ok(Array.isArray(data.scopes_supported)); + assert.deepEqual(data.bearer_methods_supported, ["header"]); + }); + + it("7.6 Auth.md serves Markdown with H1 auth.md and agent instructions", async () => { + const authMdModule = loadModule("src/pages/auth.md.ts"); + const res = await authMdModule.GET({ locals }); + assert.equal(res.status, 200); + assert.ok(res.headers.get("content-type")?.includes("text/markdown")); + const body = await res.text(); + assert.ok(body.includes("# CodingCat.dev auth.md")); + assert.ok(body.includes("/agent/claim")); + assert.ok(body.includes("/agent/auth")); + }); + }); });