const ORIGIN = 'https://developers.cloudflare.com'
const MARKDOWN_ACCEPT = 'text/markdown, text/plain;q=0.9, text/html;q=0.8'
const PREFIXES = [
'/api/',
'/fundamentals/',
'/workers/',
'/workers-ai/',
'/ai-gateway/',
'/d1/',
'/r2/',
'/kv/',
'/durable-objects/',
'/queues/',
'/vectorize/',
'/pages/',
]
function assertAllowedPath(path) {
const value = String(path || '').trim()
if (!value.startsWith('/')) {
throw new Error('path must start with `/` and must not include a host.')
}
if (!PREFIXES.some((prefix) => value.startsWith(prefix))) {
throw new Error('path must start with an allowlisted Cloudflare docs prefix.')
}
if (value.includes('..')) {
throw new Error('path must not contain `..`.')
}
if (/\s|#/.test(value)) {
throw new Error('path contains disallowed characters.')
}
if (value.length > 2048) {
throw new Error('path exceeds maximum length.')
}
return value
}
export type CloudflareDocsInput = {
path?: string
}
export type CloudflareDocsResult = {
status: number
path: string
contentType: string | null
markdownTokenEstimate: string | null
body: string
}
/**
* Fetch an allowlisted Cloudflare developer docs page as markdown-oriented text.
* @param params.path - Docs path under an allowlisted prefix such as `/workers/`.
* @returns HTTP status, content type, and truncated page body.
* @example
* import docs from 'kody:@kentcdodds/cloudflare/docs'
* const result = await docs({ path: '/workers/' })
* // => { status: 200, path: '/workers/', body: '...' }
*/
export default async function cloudflareDocs(params: CloudflareDocsInput = {}): Promise<CloudflareDocsResult> {
const path = assertAllowedPath(params.path || '/workers/')
const response = await fetch(new URL(path, ORIGIN).toString(), {
headers: { Accept: MARKDOWN_ACCEPT },
})
const body = await response.text()
return {
status: response.status,
path,
contentType: response.headers.get('content-type'),
markdownTokenEstimate: response.headers.get('x-markdown-tokens'),
body: body.slice(0, 500000),
}
}