@@ -6,6 +6,13 @@ import { HTTPException } from "hono/http-exception"
66import { trimTrailingSlash } from "hono/trailing-slash"
77
88import { NotFoundError } from "./lib/fetch"
9+ import {
10+ assertExternalDocumentationAccess ,
11+ extractExternalDocumentationBasePath ,
12+ ExternalAccessError ,
13+ fetchExternalDocCJSON ,
14+ validateExternalDocumentationUrl ,
15+ } from "./lib/external"
916import {
1017 fetchHIGPageData ,
1118 fetchHIGTableOfContents ,
@@ -19,6 +26,8 @@ import { generateAppleDocUrl, isValidAppleDocUrl, normalizeDocumentationPath } f
1926interface Env {
2027 ASSETS : Fetcher
2128 NODE_ENV : string
29+ EXTERNAL_DOC_HOST_ALLOWLIST ?: string
30+ EXTERNAL_DOC_HOST_BLOCKLIST ?: string
2231}
2332
2433const app = new Hono < { Bindings : Env } > ( )
@@ -63,6 +72,8 @@ app.all("/mcp", async (c) => {
6372 return transport . handleRequest ( c )
6473} )
6574
75+ app . get ( "/bot" , ( c ) => c . redirect ( "/#bot" , 302 ) )
76+
6677app . get ( "/documentation/*" , async ( c ) => {
6778 const path = c . req . path
6879
@@ -135,6 +146,50 @@ This service only works with Apple Developer documentation URLs:
135146 } )
136147} )
137148
149+ app . get ( "/external/*" , async ( c ) => {
150+ const path = c . req . path
151+ const rawTarget = decodeURIComponent ( path . replace ( "/external/" , "" ) )
152+ const targetUrl = validateExternalDocumentationUrl ( rawTarget )
153+
154+ await assertExternalDocumentationAccess ( targetUrl , c . env )
155+ const jsonData = await fetchExternalDocCJSON ( targetUrl )
156+ const externalBasePath = extractExternalDocumentationBasePath ( targetUrl )
157+ const markdown = await renderFromJSON ( jsonData , targetUrl . toString ( ) , {
158+ externalOrigin : `${ targetUrl . origin } ${ externalBasePath } ` ,
159+ } )
160+
161+ if ( ! markdown || markdown . trim ( ) . length < 100 ) {
162+ throw new HTTPException ( 502 , {
163+ message :
164+ "The external documentation page loaded but contained insufficient content. This may be a temporary issue with the page." ,
165+ } )
166+ }
167+
168+ const headers = {
169+ "Content-Type" : "text/markdown; charset=utf-8" ,
170+ "Content-Location" : targetUrl . toString ( ) ,
171+ "Cache-Control" : "public, max-age=3600, s-maxage=86400" ,
172+ ETag : `"${ Buffer . from ( markdown ) . toString ( "base64" ) . slice ( 0 , 16 ) } "` ,
173+ "Last-Modified" : new Date ( ) . toUTCString ( ) ,
174+ }
175+
176+ if ( c . req . header ( "Accept" ) ?. includes ( "application/json" ) ) {
177+ return c . json (
178+ {
179+ url : targetUrl . toString ( ) ,
180+ content : markdown ,
181+ } ,
182+ 200 ,
183+ { ...headers , "Content-Type" : "application/json; charset=utf-8" } ,
184+ )
185+ }
186+
187+ return c . text ( markdown , 200 , {
188+ ...headers ,
189+ "Content-Type" : "text/markdown; charset=utf-8" ,
190+ } )
191+ } )
192+
138193app . get ( "/design/human-interface-guidelines" , async ( c ) => {
139194 // Handle the table of contents for HIG
140195 const tocData = await fetchHIGTableOfContents ( )
@@ -275,6 +330,36 @@ The requested Apple Developer documentation page does not exist.
275330 )
276331 }
277332
333+ if ( err instanceof ExternalAccessError ) {
334+ const accept = c . req . header ( "Accept" )
335+ if ( accept ?. includes ( "application/json" ) ) {
336+ return c . json (
337+ {
338+ error : "External documentation access denied" ,
339+ message : err . message ,
340+ } ,
341+ { status : err . status as 400 | 403 } ,
342+ )
343+ }
344+
345+ return c . text (
346+ `# External Documentation Access Denied
347+
348+ ${ err . message }
349+
350+ ## Opt-out controls supported
351+
352+ - \`robots.txt\` disallow for \`sosumi-ai-bot\` (or \`*\`)
353+ - \`X-Robots-Tag\` response directives such as \`noai\`, \`noimageai\`, \`noindex\`
354+ - Local operator host controls: \`EXTERNAL_DOC_HOST_ALLOWLIST\`, \`EXTERNAL_DOC_HOST_BLOCKLIST\`
355+
356+ ---
357+ *[sosumi.ai](https://sosumi.ai) - Making docs AI-readable*` ,
358+ err . status as 400 | 403 ,
359+ { "Content-Type" : "text/markdown; charset=utf-8" } ,
360+ )
361+ }
362+
278363 // Handle unexpected errors
279364 const accept = c . req . header ( "Accept" )
280365 if ( accept ?. includes ( "application/json" ) ) {
0 commit comments