Skip to content

Built for people who want to own their automations. Join the waitlist for an invite.

Package listing

@kentcdodds/cloudflare

src/docs.ts

72 lines · 2.1 KB · TypeScript
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),
  }
}