Full Code of azurystudio/cheetah for AI

dev 74e046fca2b5 cached
73 files
160.7 KB
44.5k tokens
187 symbols
1 requests
Download .txt
Repository: azurystudio/cheetah
Branch: dev
Commit: 74e046fca2b5
Files: 73
Total size: 160.7 KB

Directory structure:
gitextract_960kgzwh/

├── .gitattributes
├── .github/
│   ├── dependabot.yml
│   ├── funding.yml
│   └── workflows/
│       ├── check.yml
│       ├── publish.yml
│       └── update.yml
├── .vscode/
│   ├── extensions.json
│   └── settings.json
├── base.ts
├── cheetah.ts
├── cli/
│   ├── cmd/
│   │   ├── bundle/
│   │   │   ├── bundle.ts
│   │   │   └── mod.ts
│   │   ├── new/
│   │   │   └── mod.ts
│   │   ├── random/
│   │   │   ├── create_crypto_key.ts
│   │   │   ├── create_jwt_secret.ts
│   │   │   └── mod.ts
│   │   └── serve/
│   │       └── mod.ts
│   ├── mod.ts
│   └── utils.ts
├── codeowners
├── collection.ts
├── context.ts
├── contributing.md
├── crypto.ts
├── deno.json
├── env.d.ts
├── ext/
│   ├── compress.ts
│   ├── debug.ts
│   ├── favicon.ts
│   ├── files.ts
│   ├── firewall.ts
│   ├── helmet.ts
│   └── pretty.ts
├── extensions.ts
├── handler.ts
├── jwt.ts
├── license
├── location_data.ts
├── mod.ts
├── oauth/
│   ├── client.ts
│   ├── get_session_data.ts
│   ├── get_session_id.ts
│   ├── get_session_token.ts
│   ├── handle_callback.ts
│   ├── is_signed_in.ts
│   ├── mod.ts
│   ├── sign_in.ts
│   ├── sign_out.ts
│   ├── store.ts
│   └── types.ts
├── otp.ts
├── readme.md
├── render.ts
├── request_context.ts
├── response_context.ts
├── send_mail.ts
└── test/
    ├── context.test.ts
    ├── cors.test.ts
    ├── exception.test.ts
    ├── ext/
    │   ├── favicon.test.ts
    │   ├── files.test.ts
    │   ├── firewall.test.ts
    │   └── pretty.test.ts
    ├── extensions.test.ts
    ├── jwt.test.ts
    ├── many_handlers.test.ts
    ├── preflight_mode.test.ts
    ├── render.test.tsx
    ├── request.test.ts
    ├── response.test.ts
    ├── routing/
    │   └── versioning.test.ts
    ├── routing.test.ts
    └── validation.test.ts

================================================
FILE CONTENTS
================================================

================================================
FILE: .gitattributes
================================================
* text=auto eol=lf


================================================
FILE: .github/dependabot.yml
================================================
version: 2
updates:
  - package-ecosystem: 'github-actions'
    directory: '/'
    schedule:
      interval: 'daily'


================================================
FILE: .github/funding.yml
================================================
github: 'boywithkeyboard'


================================================
FILE: .github/workflows/check.yml
================================================
name: check

on:
  - push
  - pull_request

jobs:
  check:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - uses: actions/cache@v3
        with:
          path: |
            ~/.cache/deno
            ~/.deno
          key: ${{ runner.os }}-deno-${{ hashFiles('**/*') }}
          restore-keys: |
            ${{ runner.os }}-deno-

      - name: Setup Deno
        uses: denoland/setup-deno@v1
        with:
          deno-version: v1.x

      - name: Run deno fmt
        run: deno fmt --check

      - name: Run deno lint
        run: deno lint

      - name: Run deno test
        run: deno task test --fail-fast


================================================
FILE: .github/workflows/publish.yml
================================================
name: publish

on:
  workflow_dispatch:
    inputs:
      kind:
        description: Kind of release
        default: minor
        type: choice
        options:
        - prepatch
        - patch
        - preminor
        - minor
        - premajor
        - major
        required: true

jobs:
  publish:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: Publish update
        uses: boywithkeyboard/publisher@v2
        with:
          kind: ${{github.event.inputs.kind}}
          mention_contributors: true


================================================
FILE: .github/workflows/update.yml
================================================
name: update

on:
  schedule:
    - cron: '0 0 * * *'
  workflow_dispatch:

permissions:
  contents: write
  pull-requests: write

jobs:
  update:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: Setup Deno
        uses: denoland/setup-deno@v1
        with:
          deno-version: v1.x

      - name: Run update
        run: |
          deno run -Ar https://deno.land/x/update/mod.ts -c
          CHANGELOG=$(cat updates_changelog.md)
          echo "CHANGELOG<<EOF" >> $GITHUB_ENV
          echo "$CHANGELOG" >> $GITHUB_ENV
          echo "EOF" >> $GITHUB_ENV
          rm updates_changelog.md

      - name: Create pull request
        uses: peter-evans/create-pull-request@v5
        with:
          title: 'refactor: update deps'
          author: 'github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>'
          committer: 'github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>'
          commit-message: 'refactor: update deps'
          body: '${{ env.CHANGELOG }}'
          labels: 'deps'
          delete-branch: true
          branch: 'refactor/deps'


================================================
FILE: .vscode/extensions.json
================================================
{
  "recommendations": [
    "denoland.vscode-deno",
    "gruntfuggly.todo-tree",
    "wayou.vscode-todo-highlight"
  ]
}


================================================
FILE: .vscode/settings.json
================================================
{
  "deno.enable": true,
  "deno.unstable": true,
  "editor.defaultFormatter": "denoland.vscode-deno",
  "[typescript]": {
    "editor.defaultFormatter": "denoland.vscode-deno"
  },
  "[typescriptreact]": {
    "editor.defaultFormatter": "denoland.vscode-deno"
  },
  "editor.formatOnSave": true
}


================================================
FILE: base.ts
================================================
// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.
import { bodylessHandler, handler, HandlerOrSchema } from './handler.ts'

export type Method =
  | 'delete'
  | 'get'
  | 'head'
  | 'patch'
  | 'post'
  | 'put'

export const METHODS: Method[] = [
  'delete',
  'get',
  'head',
  'patch',
  'post',
  'put',
]

export function base<T>(): {
  new (
    addRoute: (
      method: Uppercase<Method>,
      pathname: string,
      handlers: HandlerOrSchema[],
    ) => unknown,
  ):
    & {
      [
        M in (
          | 'delete'
          | 'patch'
          | 'post'
          | 'put'
        )
      ]: ReturnType<typeof handler<T>>
    }
    & {
      [
        M in (
          | 'get'
          | 'head'
        )
      ]: ReturnType<typeof bodylessHandler<T>>
    }
} {
  return class {
    #a

    constructor(
      addRoute: (
        method: Uppercase<Method>,
        pathname: string,
        handlers: HandlerOrSchema[],
      ) => unknown,
    ) {
      this.#a = addRoute
    }

    delete(pathname: string, ...handlers: HandlerOrSchema[]) {
      return this.#a('DELETE', pathname, handlers)
    }

    get(pathname: string, ...handlers: HandlerOrSchema[]) {
      return this.#a('GET', pathname, handlers)
    }

    head(pathname: string, ...handlers: HandlerOrSchema[]) {
      return this.#a('HEAD', pathname, handlers)
    }

    patch(pathname: string, ...handlers: HandlerOrSchema[]) {
      return this.#a('PATCH', pathname, handlers)
    }

    post(pathname: string, ...handlers: HandlerOrSchema[]) {
      return this.#a('POST', pathname, handlers)
    }

    put(pathname: string, ...handlers: HandlerOrSchema[]) {
      return this.#a('PUT', pathname, handlers)
    }
  } as never
}


================================================
FILE: cheetah.ts
================================================
// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.
import { base, Method } from './base.ts'
import { Collection } from './collection.ts'
import { Context, Exception } from './context.ts'
import { Extension, validExtension } from './extensions.ts'
import {
  Handler,
  HandlerOrSchema,
  Payload,
  Version,
  VersionRange,
} from './handler.ts'
import { OAuthStore } from './oauth/mod.ts'
import { OAuthSessionData } from './oauth/types.ts'
import { ResponseContext } from './response_context.ts'

export type AppContext = {
  debugging: boolean
  gateway?: number
  env: Record<string, unknown> | undefined
  ip: string
  proxy: AppConfig['proxy']
  routes: cheetah['routes']
  runtime:
    | 'cloudflare'
    | 'deno'
  request: {
    pathname: string
    querystring?: string
  }
  oauth: AppConfig['oauth']
  versioning: AppConfig['versioning']
  caching: AppConfig['cache']
}

export type AppConfig = {
  /**
   * A prefix for all routes, e.g. `/api`.
   *
   * @default '/'
   */
  base?: `/${string}`

  /**
   * Enable Cross-Origin Resource Sharing (CORS) for your app by setting a origin, e.g. `*`.
   */
  cors?: string

  /**
   * If enabled, cheetah will attempt to find the matching `.get()` handler for an incoming HEAD request. Your existing `.head()` handlers won't be impacted.
   *
   * @default false
   * @since v0.11
   */
  preflight?: boolean

  /**
   * If you're using Cloudflare as a proxy, you should confirm it with this setting in order to unleash the full potential of cheetah.
   *
   * @default undefined
   */
  proxy?: 'cloudflare'

  /**
   * Set a custom error handler.
   */
  error?: (error: unknown, request: Request) => Response | Promise<Response>

  /**
   * Set a custom 404 handler.
   */
  notFound?: (request: Request) => Response | Promise<Response>

  oauth?: {
    store: OAuthStore
    cookie?: Parameters<ResponseContext['setCookie']>[2]
    onSignIn?: (
      c: Context,
      data: OAuthSessionData,
    ) => Promise<unknown> | unknown
    onSignOut?: (
      c: Context,
      identifier: string,
    ) => Promise<unknown> | unknown
  }

  /**
   * If you enable debug mode, all errors will get logged to the console.
   *
   * @since v1.3
   */
  debug?: boolean

  versioning?:
    & (
      | {
        type: 'uri'
      }
      | {
        type: 'header'
        header: string
      }
    )
    & {
      current: Version
    }

  cache?: {
    /** A unique name for your cache. */
    name: string
  }
}

export class cheetah extends base<cheetah>() {
  #base
  #cors
  #error
  #extensions: Set<[string, Extension]>
  #notFound
  #preflight
  #proxy
  #routes: Set<[Uppercase<Method>, string, RegExp, HandlerOrSchema[]]>
  #runtime: 'deno' | 'cloudflare'
  #onPlugIn
  #oauth
  #debug
  #versioning
  #cache

  constructor({
    base,
    cors,
    preflight = false,
    proxy,
    error,
    notFound,
    oauth,
    debug = false,
    versioning,
    cache,
  }: AppConfig = {}) {
    super((method, pathname, handlers) => {
      pathname = this.#base ? this.#base + pathname : pathname

      this.#routes.add([
        method,
        pathname,
        RegExp(`^${
          (pathname
            .replace(/\/+(\/|$)/g, '$1'))
            .replace(/(\/?\.?):(\w+)\+/g, '($1(?<$2>*))')
            .replace(/(\/?\.?):(\w+)/g, '($1(?<$2>[^$1/]+?))')
            .replace(/\./g, '\\.')
            .replace(/(\/?)\*/g, '($1.*)?')
        }/*$`),
        handlers,
      ])

      return this
    })

    this.#base = base === '/' ? undefined : base
    this.#cors = cors
    this.#error = error
    this.#extensions = new Set()
    this.#notFound = notFound
    this.#preflight = preflight
    this.#proxy = proxy
    this.#routes = new Set()
    this.#runtime = typeof globalThis?.Deno?.serve !== 'function'
      ? 'cloudflare'
      : 'deno'
    this.#onPlugIn = false
    this.#oauth = oauth
    this.#debug = debug
    this.#versioning = versioning
    this.#cache = cache
  }

  /* use ---------------------------------------------------------------------- */

  // deno-lint-ignore no-explicit-any
  use<C extends Collection>(...extensions: Extension<any>[]): this
  use<C extends Collection>(
    prefix: `/${string}`,
    // deno-lint-ignore no-explicit-any
    ...extensions: Extension<any>[]
  ): this
  use<C extends Collection>(
    prefix: `/${string}`,
    collection: C,
    // deno-lint-ignore no-explicit-any
    ...extensions: Extension<any>[]
  ): this

  use<C extends Collection>(
    // deno-lint-ignore no-explicit-any
    ...elements: (`/${string}` | C | Extension<any>)[]
  ) {
    let pre

    for (const e of elements) {
      if (typeof e === 'string') { // prefix
        pre = e
      } else if (e instanceof Collection) { // collection
        if (!pre || pre === '/') {
          pre = ''
        }

        for (const r of e.routes.values()) {
          let pathname = r[1]

          if (pathname === '/') {
            pathname = ''
          }

          pathname = this.#base ? this.#base + pre + pathname : pre + pathname

          this.#routes.add([
            r[0],
            pathname,
            RegExp(
              `^${
                (pathname
                  .replace(/\/+(\/|$)/g, '$1'))
                  .replace(/(\/?\.?):(\w+)\+/g, '($1(?<$2>*))')
                  .replace(/(\/?\.?):(\w+)/g, '($1(?<$2>[^$1/]+?))')
                  .replace(/\./g, '\\.')
                  .replace(/(\/?)\*/g, '($1.*)?')
              }/*$`,
            ),
            r[2],
          ])
        }
      } else if (validExtension(e)) { // extension
        if (!pre) {
          pre = '*'
        }

        // @ts-ignore:
        this.#extensions.add([pre, e])
      }
    }

    return this
  }

  get routes() {
    return this.#routes
  }

  /* router ------------------------------------------------------------------- */

  #parseVersion(headers: Headers, pathname: string) {
    if (!this.#versioning) { // for typescript
      throw new Error('Versioning not configured!')
    }

    const regex = /^v[1-9][0-9]?$|^100$/

    if (this.#versioning.type === 'uri') {
      const arr = pathname.replace('/', '').split('/')

      if (this.#base) {
        if (regex.test(arr[1])) {
          const version = arr[1]

          arr.splice(1, 1)

          return { version, pathname: '/' + arr.join('/') }
        }
      } else {
        if (regex.test(arr[0])) {
          const version = arr[0]

          arr.shift()

          return { version, pathname: '/' + arr.join('/') }
        }
      }

      return { version: this.#versioning.current, pathname }
    }

    const header = headers.get(this.#versioning.header)

    if (
      this.#versioning.type === 'header' && header !== null &&
      regex.test(header)
    ) {
      return { version: header, pathname }
    }

    return { version: this.#versioning.current, pathname }
  }

  #match(request: Request, p: string) {
    for (const r of this.#routes.values()) {
      if (
        request.method === r[0] ||
        request.method === 'OPTIONS' ||
        this.#preflight && request.method === 'HEAD' && r[0] === 'GET'
      ) {
        if (this.#versioning) {
          const { pathname, version } = this.#parseVersion(request.headers, p)

          if (
            parseInt(version.replace('v', '')) >
              parseInt(this.#versioning.current.replace('v', ''))
          ) {
            break
          }

          const options = typeof r[3][0] !== 'function' ? r[3][0] : null

          if (options?.gateway !== undefined) {
            const result = pathname.match(r[2])

            if (!result) {
              continue
            }

            const gateway = isVersionWithinRange(
              version as Version,
              options.gateway as VersionRange,
            )

            if (!gateway) {
              break
            }

            return {
              handlers: r[3],
              params: result.groups ?? {},
              gateway,
            }
          } else {
            const result = pathname.match(r[2])

            if (!result) {
              continue
            }

            return {
              handlers: r[3],
              params: result.groups ?? {},
              gateway: parseInt(version.replace('v', '')),
            }
          }
        } else {
          const result = p.match(r[2])

          if (!result) {
            continue
          }

          return {
            handlers: r[3],
            params: result.groups ?? {},
            gateway: undefined,
          }
        }
      }
    }

    return null
  }

  /* fetch -------------------------------------------------------------------- */

  fetch = async (
    req: Request,
    data: Record<string, unknown> | Deno.ServeHandlerInfo = {},
    context?: {
      waitUntil: (promise: Promise<unknown>) => void
    },
  ): Promise<Response> => {
    try {
      const ip = data?.remoteAddr && this.#runtime === 'deno'
        ? (data as Deno.ServeHandlerInfo).remoteAddr
          .hostname
        : req.headers.get('cf-connecting-ip') as string

      const parts = req.url.split('?')

      parts[0] = parts[0].slice(8)

      const __app: AppContext = {
        env: data as Record<string, unknown>,
        ip,
        proxy: this.#proxy,
        request: {
          pathname: parts[0].substring(parts[0].indexOf('/')),
          querystring: parts[1],
        },
        routes: this.#routes,
        runtime: this.#runtime,
        oauth: this.#oauth,
        versioning: this.#versioning,
        gateway: -1,
        debugging: this.#debug,
        caching: this.#cache,
      }

      if (this.#extensions.size > 0) {
        let body: Response | void = undefined

        for (const e of this.#extensions.values()) {
          if (!this.#onPlugIn && e[1].onPlugIn !== undefined) {
            await e[1].onPlugIn({
              prefix: e[0],
              env: __app.env,
              routes: this.#routes,
              runtime: this.#runtime,
              settings: e[1].__config,
              setRoute: (method, pathname, ...handlers) => {
                this.#routes.add([
                  method.toUpperCase() as Uppercase<Method>,
                  pathname,
                  RegExp(
                    `^${
                      (pathname
                        .replace(/\/+(\/|$)/g, '$1'))
                        .replace(/(\/?\.?):(\w+)\+/g, '($1(?<$2>*))')
                        .replace(/(\/?\.?):(\w+)/g, '($1(?<$2>[^$1/]+?))')
                        .replace(/\./g, '\\.')
                        .replace(/(\/?)\*/g, '($1.*)?')
                    }/*$`,
                  ),
                  // @ts-ignore:
                  handlers,
                ])
              },
            })
          }

          if (
            e[0] !== '*' &&
            __app.request.pathname.indexOf(e[0]) !== 0
          ) {
            continue
          }

          if (e[1].onRequest !== undefined) {
            const result = await e[1].onRequest({
              prefix: e[0],
              app: __app,
              req,
              _: e[1].__config,
            })

            if (result !== undefined) {
              body = result
            }
          }
        }

        if (!this.#onPlugIn) {
          this.#onPlugIn = true
        }

        if (body !== undefined) {
          return body
        }
      }

      const route = this.#match(
        req,
        __app.request.pathname,
      )

      __app.gateway = route?.gateway ?? -1

      if (!route) {
        if (!this.#notFound) {
          throw new Exception('Not Found', undefined, 404)
        }

        if (req.method !== 'HEAD') {
          return await this.#notFound(req)
        }

        const response = await this.#notFound(req)

        return new Response(null, {
          headers: response.headers,
          status: response.status,
          statusText: response.statusText,
        })
      }

      const response = await this.#handle(
        __app,
        req,
        context?.waitUntil ??
          ((promise: Promise<unknown>) => {
            // deno-fmt-ignore-line
            (async () => await promise)()
          }),
        route.params,
        route.handlers,
      )

      if (req.method === 'HEAD') {
        return new Response(null, {
          headers: response.headers,
          status: response.status,
          statusText: response.statusText,
        })
      }

      return response
    } catch (err) {
      let res: Response

      if (err instanceof Exception) {
        res = err.response(req)
      } else {
        if (this.#debug) {
          console.error(err)
        }

        if (this.#error) {
          res = await this.#error(err, req)
        } else {
          res = new Exception('Something Went Wrong', undefined, 500).response(
            req,
          )
        }
      }

      if (req.method === 'HEAD') {
        return new Response(null, {
          headers: res.headers,
          status: res.status,
          statusText: res.statusText,
        })
      }

      return res
    }
  }

  /* handle ------------------------------------------------------------------- */

  async #handle(
    __app: AppContext,
    r: Request,
    waitUntil: (promise: Promise<unknown>) => void,
    p: Record<string, string | undefined>,
    handlers: HandlerOrSchema[],
  ) {
    const o = typeof handlers[0] !== 'function' ? handlers[0] : null

    // preflight cors request

    if (
      r.method === 'OPTIONS' &&
      r.headers.has('origin') &&
      r.headers.has('access-control-request-method')
    ) {
      return new Response(null, {
        status: 204,
        headers: {
          ...((this.#cors || o?.cors) &&
            { 'access-control-allow-origin': o?.cors ?? this.#cors }),
          'access-control-allow-methods': '*',
          'access-control-allow-headers':
            r.headers.get('access-control-request-headers') ?? '*',
          'access-control-allow-credentials': 'false',
          'access-control-max-age': '600',
        },
      })
    }

    // construct context

    const $: {
      b: Exclude<Payload, void> | null
      c: number
      h: Headers
    } = {
      b: null,
      c: 200,
      h: new Headers({
        ...(this.#cors && {
          'access-control-allow-origin': this.#cors,
        }),
      }),
    }

    const context = new Context(
      __app,
      $,
      p,
      r,
      o,
      waitUntil,
    )

    // handle request

    const len = handlers.length

    let next = true

    for (let i = 0; i < len; ++i) {
      if (typeof handlers[i] !== 'function') {
        continue
      }

      if (!next) {
        break
      }

      next = false

      const result = await (handlers[i] as Handler<unknown>)(
        context,
        () => {
          next = true
        },
      )

      if (result) {
        $.b = result
      }
    }

    // construct response

    if ($.c.toString().indexOf('3') === 0) {
      return new Response(null, {
        headers: $.h,
        status: $.c,
      })
    }

    if (!$.b) {
      return new Response(null, {
        headers: $.h,
        status: $.c,
      })
    }

    for (const e of this.#extensions.values()) {
      if (
        e[0] !== '*' &&
        __app.request.pathname.indexOf(e[0]) !== 0
      ) {
        continue
      }

      const { onResponse } = e[1]

      if (onResponse !== undefined) {
        onResponse({
          prefix: e[0],
          app: __app,
          c: context,
          _: e[1].__config,
        })
      }
    }

    switch ($.b.constructor.name) {
      case 'String': {
        if (!$.h.has('content-type')) {
          $.h.set('content-type', 'text/plain; charset=utf-8')
        }

        break
      }

      case 'Object': {
        try {
          if ((($.b as unknown) as { code: number }).code) {
            $.c = (($.b as unknown) as { code: number }).code
          }

          $.b = JSON.stringify($.b)

          if (!$.h.has('content-type')) {
            $.h.set('content-type', 'application/json; charset=utf-8')
          }
        } catch (err) {
          console.log(err)
        }

        break
      }

      case 'Array': {
        $.b = JSON.stringify($.b)

        if (!$.h.has('content-type')) {
          $.h.set('content-type', 'application/json; charset=utf-8')
        }

        break
      }

      default:
        break
    }

    return new Response($.b as BodyInit, {
      headers: $.h,
      status: $.c,
    })
  }

  /* serve -------------------------------------------------------------------- */

  serve({
    hostname,
    port,
  }: {
    hostname?: string
    port?: number
  } = {}) {
    return Deno.serve({
      hostname,
      port,
    }, (request, data) => {
      return this.fetch(request, data)
    }).finished
  }
}

function isVersionWithinRange(
  version: Version,
  r: VersionRange,
): number | undefined {
  const v = parseInt(version.replace('v', ''))

  if (parseInt(r.replace('v', '')) === v) {
    return v
  }

  if (r.startsWith('v') && r.includes('...')) { // from (min) ... to (max)
    const from = parseInt(r.split('...')[0].replace('v', ''))
    const to = parseInt(r.split('...')[1].replace('v', ''))

    return v >= from && v <= to ? v : undefined
  } else if (r.startsWith('> ')) {
    return v > parseInt(r.replace('> v', '')) ? v : undefined
  } else if (r.startsWith('< ')) {
    return v < parseInt(r.replace('< v', '')) ? v : undefined
  } else if (r.startsWith('>= ')) {
    return v >= parseInt(r.replace('>= v', '')) ? v : undefined
  } else if (r.startsWith('<= ')) {
    return v <= parseInt(r.replace('<= v', '')) ? v : undefined
  }

  return undefined
}


================================================
FILE: cli/cmd/bundle/bundle.ts
================================================
// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.
import { ensureFile } from 'std/fs/ensure_file.ts'
import { join } from 'std/path/mod.ts'
import * as esbuild from 'esbuild'

export async function bundle({
  input = './mod.ts',
  output = './mod.js',
  banner = '// deno-fmt-ignore-file\n// deno-lint-ignore-file',
  target = 'es2022',
  cwd = Deno.cwd(),
  runtime: _ = 'cloudflare',
}: {
  /**
   * @default './mod.ts'
   */
  input?: string
  /**
   * @default './mod.js'
   */
  output?: string
  /**
   * @default '// deno-fmt-ignore-file\n// deno-lint-ignore-file'
   */
  banner?: string
  /**
   * @default 'es2022'
   */
  target?:
    | 'es2015'
    | 'es2016'
    | 'es2017'
    | 'es2018'
    | 'es2019'
    | 'es2020'
    | 'es2021'
    | 'es2022'
  /**
   * @default Deno.cwd()
   */
  cwd?: string
  /**
   * @default 'deno'
   */
  runtime?:
    | 'cloudflare'
    | 'deno'
}) {
  const hasImportMap = async (path: string) => {
    try {
      const content = await Deno.readTextFile(join(cwd, path))

      if (!content) {
        return false
      }

      return JSON.parse(content).imports !== undefined
    } catch (_) {
      return false
    }
  }

  let options: string[] = []

  if (await hasImportMap('deno.json')) {
    options = ['--config', 'deno.json']
  } else if (await hasImportMap('deno.jsonc')) {
    options = ['--config', 'deno.jsonc']
  } else if (await hasImportMap('import_map.json')) {
    options = ['--import-map', 'deno.json']
  } else if (await hasImportMap('importMap.json')) {
    options = ['--import-map', 'importMap.json']
  } else if (await hasImportMap('imports.json')) {
    options = ['--import-map', 'imports.json']
  }

  ensureFile(output)

  const cmd = new Deno.Command('deno', {
    args: ['bundle', '-q', '--unstable', ...options, input, output],
    cwd,
  })

  await cmd.output()

  await esbuild.build({
    entryPoints: [output],
    bundle: true,
    minify: true,
    format: 'esm',
    allowOverwrite: true,
    target,
    banner: {
      js: banner,
    },
    outfile: output,
    absWorkingDir: cwd,
  })

  esbuild.stop()

  return {
    outputSize: (await Deno.readTextFile(join(cwd, output))).length,
  }
}


================================================
FILE: cli/cmd/bundle/mod.ts
================================================
// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.
import { parse } from 'https://deno.land/std@0.203.0/flags/mod.ts'
import {
  brightGreen,
  brightRed,
  brightYellow,
  gray,
} from 'https://deno.land/std@0.203.0/fmt/colors.ts'
import byte from 'https://deno.land/x/byte@v3.3.0/byte.ts'
import { logError } from '../../utils.ts'
import { bundle } from './bundle.ts'

export async function bundleCommand(args: ReturnType<typeof parse>) {
  const { _, target, runtime } = args

  const input = _[1] && typeof _[1] === 'string' ? _[1] : undefined
  const output = _[2] && typeof _[2] === 'string' ? _[2] : undefined

  try {
    const { outputSize } = await bundle({
      input,
      output,
      // @ts-ignore:
      target: typeof target === 'string' ? target : undefined,
      // @ts-ignore:
      runtime: typeof runtime === 'string' ? runtime : undefined,
    })

    console.info(
      gray(
        `file size - ${
          outputSize < 1_000_000
            ? brightGreen(byte(outputSize))
            : outputSize < 5_000_000
            ? brightYellow(byte(outputSize))
            : brightRed(byte(outputSize))
        }`,
      ),
    )
  } catch (err) {
    if (err instanceof Error) {
      logError(err.message)
    } else {
      logError('something went wrong trying to bundle your app.')
    }
  }
}


================================================
FILE: cli/cmd/new/mod.ts
================================================
// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.
import { ensureFile } from 'https://deno.land/std@0.203.0/fs/ensure_file.ts'
import { Select } from 'cliffy'

type VSCodeSettings = {
  files: string[]
  vscode: Record<string, unknown>
  imports: string[]
}

export async function newCommand() {
  const platform = await Select.prompt({
    message: 'Where do you plan to deploy your app?',
    options: [
      { name: 'Deno 🦕', value: 'deno' },
      { name: 'Cloudflare Workers ⚡', value: 'cloudflare' },
    ],
  })

  let url =
    `https://raw.githubusercontent.com/boywithkeyboard/templates/dev/${platform}/`

  url += 'starter/'

  const meta: VSCodeSettings = JSON.parse(
    await (await fetch(url + 'meta.json')).text(),
  )

  const importMap: Record<string, string> = JSON.parse(
    await (await fetch(
      'https://raw.githubusercontent.com/boywithkeyboard/templates/dev/deno.json',
    )).text(),
  ).imports

  for (const f of meta.files) {
    const res = await fetch(url + f)

    await ensureFile(f)
    await Deno.writeTextFile(f, await res.text())
  }

  const imports = {}

  for (const i of meta.imports) {
    // @ts-ignore:
    imports[i] = importMap[i]
  }

  await ensureFile('.vscode/settings.json')
  await Deno.writeTextFile('.vscode/settings.json', JSON.stringify(meta.vscode))

  if (platform === 'cloudflare') {
    await Deno.writeTextFile(
      'wrangler.toml',
      `name = "<name>"
account_id = "<account_id>"
route = "<route>"
compatibility_date = "2023-03-24"
    
[build]
command = "deno task build"`,
    )
  }

  const version =
    (await (await fetch('https://apiland.deno.dev/v2/modules/cheetah')).json())
      .latest_version

  await Deno.writeTextFile(
    'deno.json',
    JSON.stringify(
      {
        fmt: {
          semiColons: false,
          singleQuote: true,
        },
        imports,
        tasks: {
          build: `deno run -A https://deno.land/x/cheetah@${version}/build.ts`,
        },
      },
      null,
      2,
    ),
  )

  const command = new Deno.Command('deno', {
    args: ['fmt', '--single-quote', '--no-semicolons'],
    stdout: 'piped',
  })

  command.spawn()
}


================================================
FILE: cli/cmd/random/create_crypto_key.ts
================================================
// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.
import { encode } from 'std/encoding/base64.ts'

export async function createCryptoKey() {
  const key = await crypto.subtle.generateKey(
    { name: 'AES-GCM', hash: 'SHA-512', length: 256 },
    true,
    ['encrypt', 'decrypt'],
  )

  const exportedKey = await crypto.subtle.exportKey('raw', key)

  return encode(exportedKey)
}


================================================
FILE: cli/cmd/random/create_jwt_secret.ts
================================================
// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.
import { encode } from 'std/encoding/base64.ts'

export async function createJwtSecret() {
  const key = await crypto.subtle.generateKey(
    { name: 'HMAC', hash: 'SHA-512' },
    true,
    ['sign', 'verify'],
  )

  const exportedKey = await crypto.subtle.exportKey('raw', key)

  return encode(exportedKey)
}


================================================
FILE: cli/cmd/random/mod.ts
================================================
// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.
import { Select } from 'cliffy'
import { gray, white } from 'std/fmt/colors.ts'
import { createCryptoKey } from './create_crypto_key.ts'
import { createJwtSecret } from './create_jwt_secret.ts'

export async function randomCommand() {
  const type: string = await Select.prompt({
    message: 'What do you want to create?',
    options: [
      { name: 'JWT Secret', value: 'jwt_secret' },
      { name: 'Crypto Key', value: 'crypto_key' },
    ],
  })

  if (type === 'jwt_secret') {
    console.info(
      gray(`🗝️ ${white(type.replace('_', ' '))} - ${await createJwtSecret()}`),
    )
  } else {
    console.info(
      gray(`🗝️ ${white(type.replace('_', ' '))} - ${await createCryptoKey()}`),
    )
  }
}


================================================
FILE: cli/cmd/serve/mod.ts
================================================
// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.
import { keypress, KeyPressEvent } from 'cliffy/keypress'
import { loadSync } from 'std/dotenv/mod.ts'
import { parse } from 'std/flags/mod.ts'
import { brightGreen, gray, white } from 'std/fmt/colors.ts'
import { cheetah } from '../../../cheetah.ts'
import { logError } from '../../utils.ts'

export async function serveCommand(args: ReturnType<typeof parse>) {
  if (typeof args._[1] !== 'string') {
    return logError('please specify an entry point')
  }

  loadSync({ export: true })

  Deno.env.set('DEV', 'true')

  const exports = await import(args._[1] as string)

  let childProcess: Deno.ChildProcess

  if (exports.fetch || exports.default && exports.default instanceof cheetah) {
    if (exports.fetch) {
      Deno.writeTextFileSync(
        './__app.js',
        `const { fetch: f } = await import('${
          args._[1]
        }')\nDeno.serve((r, d) => f(r, d))`,
      )

      const cmd = new Deno.Command('deno', {
        args: ['run', '-A', '--watch', '__app.js'],
      })

      childProcess = cmd.spawn()
    } else {
      Deno.writeTextFileSync(
        './__app.js',
        `const { default: a } = await import('${
          args._[1]
        }')\nDeno.serve((r, d) => a.fetch(r, d))`,
      )

      const cmd = new Deno.Command('deno', {
        args: ['run', '-A', '--watch', '__app.js'],
      })

      childProcess = cmd.spawn()
    }
  } else {
    const cmd = new Deno.Command('deno', {
      args: ['run', '-A', '--watch', args._[1]],
    })

    childProcess = cmd.spawn()
  }

  console.info(
    gray(`${brightGreen('success')} - press ${white('CTRL+C')} to exit`),
  )

  for await (const e of keypress()) {
    const { ctrlKey, key } = e as KeyPressEvent

    if (ctrlKey && key === 'c' || key === 'escape') {
      try {
        Deno.removeSync('./__app.js')
      } catch (_err) {
        //
      }

      childProcess.kill()

      Deno.exit()
    }
  }
}


================================================
FILE: cli/mod.ts
================================================
// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.
import { parse } from 'std/flags/mod.ts'
import { bundleCommand } from './cmd/bundle/mod.ts'
import { newCommand } from './cmd/new/mod.ts'
import { randomCommand } from './cmd/random/mod.ts'
import { serveCommand } from './cmd/serve/mod.ts'
import { logError } from './utils.ts'

const args = parse(Deno.args)

if (args._[0] === 'bundle') {
  bundleCommand(args)
} else if (args._[0] === 'random') {
  randomCommand()
} else if (args._[0] === 'new') {
  newCommand()
} else if (args._[0] === 'serve') {
  serveCommand(args)
} else {
  logError('unknown command')
}


================================================
FILE: cli/utils.ts
================================================
// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.
import { brightRed, gray } from 'std/fmt/colors.ts'

export function logError(message: string) {
  console.error(gray(`${brightRed('error')} - ${message}`))
}


================================================
FILE: codeowners
================================================
* @boywithkeyboard


================================================
FILE: collection.ts
================================================
// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.
import { base, Method } from './base.ts'
import { HandlerOrSchema } from './handler.ts'

export class Collection extends base<Collection>() {
  #cors:
    | string
    | undefined

  routes: Set<[
    Uppercase<Method>,
    string,
    HandlerOrSchema[],
  ]>

  constructor({
    cors,
  }: {
    /**
     * Enable Cross-Origin Resource Sharing (CORS) for this collection by setting a origin, e.g. `*`.
     *
     * @since v0.11
     */
    cors?: string
  } = {}) {
    super((method, pathname, handlers) => {
      if (this.#cors && typeof handlers[0] !== 'function') {
        if (!handlers[0].cors) {
          handlers[0].cors = this.#cors
        }
      }

      this.routes.add([method, pathname, handlers])

      return this
    })

    this.#cors = cors
    this.routes = new Set()
  }
}


================================================
FILE: context.ts
================================================
// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.
/// <reference types='./env.d.ts' />
import { encode } from 'std/encoding/base64.ts'
import { ZodType } from 'zod'
import { AppContext } from './cheetah.ts'
import { ObjectType, Payload } from './handler.ts'
import { RequestContext } from './request_context.ts'
import { ResponseContext } from './response_context.ts'

const HTTP_MESSAGES = {
  'Bad Request': 400,
  'Unauthorized': 401,
  'Access Denied': 403,
  'Not Found': 404,
  'Method Not Allowed': 405,
  'Not Acceptable': 406,
  'Request Timeout': 408,
  'Conflict': 409,
  'Gone': 410,
  'Length Required': 411,
  'Precondition Failed': 412,
  'Content Too Large': 413,
  'URI Too Long': 414,
  'Unsupported Media Type': 415,
  'Range Not Satisfiable': 416,
  'Expectation Failed': 417,
  'Teapot': 418,
  'Misdirected': 421,
  'Upgrade Required': 426,
  'Precondition Required': 428,
  'Rate Limit Exceeded': 429,
  'Regional Ban': 451,
  'Something Went Wrong': 500,
}

export class Context<
  Params extends Record<string, unknown> = Record<string, unknown>,
  ValidatedBody extends ZodType = never,
  ValidatedCookies extends ObjectType = never,
  ValidatedHeaders extends ObjectType = never,
  ValidatedQuery extends ObjectType = never,
> {
  #a
  #c: Cache | undefined
  #i
  #p
  #r
  #s
  #req:
    | RequestContext<
      Params,
      ValidatedBody,
      ValidatedCookies,
      ValidatedHeaders,
      ValidatedQuery
    >
    | undefined
  #res: ResponseContext | undefined
  /**
   * Wait until the response is sent to the client, then resolve the promise.
   */
  waitUntil: (promise: Promise<unknown>) => void

  constructor(
    __app: AppContext,
    __internal: {
      b: Exclude<Payload, void> | null
      c: number
      h: Headers
    },
    p: Record<string, string | undefined>,
    r: Request,
    s: {
      body?: ZodType | undefined
      cookies?: ObjectType | undefined
      headers?: ObjectType | undefined
      query?: ObjectType | undefined
      [key: string]: unknown
    } | null,
    waitUntil: (promise: Promise<unknown>) => void,
  ) {
    this.#a = __app
    this.#i = __internal
    this.#p = p
    this.#r = r
    this.#s = s
    this.waitUntil = waitUntil
  }

  get __app(): AppContext {
    return this.#a
  }

  get cache(): Cache {
    if (this.#c) {
      return this.#c
    }

    this.#c = new Cache(this)

    return this.#c
  }

  get dev(): boolean {
    return this.#a.debugging ||
      this.runtime === 'deno' && Deno.env.get('DEV') === 'true'
  }

  env<T extends keyof Variables>(name: T): Variables[T] {
    return this.runtime === 'deno'
      ? Deno.env.get(name)
      : (this.#a.env as Variables)[name]
  }

  get req(): RequestContext<
    Params,
    ValidatedBody,
    ValidatedCookies,
    ValidatedHeaders,
    ValidatedQuery
  > {
    if (this.#req) {
      return this.#req
    }

    this.#req = new RequestContext(
      this.#a,
      this.#p,
      this.#r,
      this.#s,
      this.exception,
    )

    return this.#req
  }

  get res(): ResponseContext {
    if (this.#res) {
      return this.#res
    }

    this.#res = new ResponseContext(this.#i)

    return this.#res
  }

  get runtime() {
    return this.#a.runtime
  }

  exception(error: keyof typeof HTTP_MESSAGES, description?: string) {
    const code = HTTP_MESSAGES[error]

    return new Exception(error, description, code)
  }
}

/** @private */
export class Exception {
  public response

  constructor(
    error: string,
    description: string | undefined,
    code: number,
  ) {
    this.response = (request: Request) => {
      const a = request.headers.get('accept')

      const json = a
        ? a.indexOf('application/json') > -1 ||
          a.indexOf('*/*') > -1
        : false

      return new Response(
        json ? JSON.stringify({ error, description, code }) : error,
        {
          headers: {
            'content-type': `${
              json ? 'application/json' : 'text/plain'
            }; charset=utf-8;`,
          },
          status: code,
        },
      )
    }
  }
}

class Cache {
  #cache: globalThis.Cache | null
  #context
  #name

  constructor(c: Context) {
    this.#cache = null
    this.#context = c
    this.#name = c.__app.caching?.name ?? 'cheetah'
  }

  async set(
    key: string,
    value: string | Record<string, unknown> | Uint8Array,
    options?: {
      maxAge?: number
    },
  ) {
    if (this.#cache === null) {
      this.#cache = await caches.open(this.#name)
    }

    this.#context.waitUntil(
      this.#cache.put(
        `https://${this.#name}.com/${encode(key)}`,
        new Response(
          typeof value === 'string' || value instanceof Uint8Array
            ? value
            : JSON.stringify(value),
          {
            headers: {
              'cache-control': `max-age=${options?.maxAge ?? 300}`,
            },
          },
        ),
      ),
    )
  }

  get<T extends Record<string, unknown> = Record<string, unknown>>(
    key: string,
    type: 'json',
  ): Promise<T | undefined>
  get<T extends string = string>(
    key: string,
    type: 'string',
  ): Promise<T | undefined>
  get<T extends Uint8Array = Uint8Array>(
    key: string,
    type: 'buffer',
  ): Promise<T | undefined>

  async get<T extends Record<string, unknown> | string | Uint8Array>(
    key: string,
    type: 'string' | 'json' | 'buffer' = 'string',
  ): Promise<T | undefined> {
    if (this.#cache === null) {
      this.#cache = await caches.open(this.#name)
    }

    try {
      const result = await this.#cache.match(
        `https://${this.#name}.com/${encode(key)}`,
      )

      if (!result) {
        return undefined
      }

      const data = type === 'string'
        ? await result.text()
        : type === 'json'
        ? await result.json()
        : new Uint8Array(await result.arrayBuffer())

      return data
    } catch (_err) {
      return undefined
    }
  }

  async has(key: string) {
    if (this.#cache === null) {
      this.#cache = await caches.open(this.#name)
    }

    const result = await this.#cache.match(
      `https://${this.#name}.com/${encode(key)}`,
    )

    return result !== undefined
  }

  async delete(key: string) {
    if (this.#cache === null) {
      this.#cache = await caches.open(this.#name)
    }

    this.#context.waitUntil(
      this.#cache.delete(
        `https://${this.#name}.com/${encode(key)}`,
      ),
    )
  }
}


================================================
FILE: contributing.md
================================================
### Follow Conventional Commits

This repository enforces the
[Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/)
specification for writing pull requests and issues titles.

### Write a meaningful title

We ask you to write a short and meaningful title while going into more detail
about your changes in the description of the pull request.

Typically, you should receive a response to your pull request within a few
hours.

### Make sure nothing's broken

After you've made your changes, please run `deno task check` to format & check
your code and run the test suites.


================================================
FILE: crypto.ts
================================================
// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.
import { decode } from 'std/encoding/base64.ts'
import { Context } from './context.ts'

export async function encrypt(c: Context, message: string) {
  const key = (c.env('crypto_key') ?? c.env('CRYPTO_KEY')) as string

  const iv = crypto.getRandomValues(new Uint8Array(12))
  const ivStr = Array.from(iv)
    .map((byte) => String.fromCharCode(byte))
    .join('')
  const alg = { name: 'AES-GCM', iv }
  const cryptoKey = await crypto.subtle.importKey(
    'raw',
    decode(key).buffer,
    alg,
    true,
    ['encrypt', 'decrypt'],
  )
  const cipherBuf = await crypto.subtle.encrypt(
    alg,
    cryptoKey,
    new TextEncoder().encode(message),
  )
  const cipherArr = Array.from(new Uint8Array(cipherBuf))
  const cipherStr = cipherArr.map((byte) => String.fromCharCode(byte))
    .join('')

  return btoa(ivStr + cipherStr)
}

export async function decrypt(c: Context, message: string) {
  const key = (c.env('crypto_key') ?? c.env('CRYPTO_KEY')) as string

  const iv = atob(message).slice(0, 12)
  const alg = {
    name: 'AES-GCM',
    iv: new Uint8Array(
      Array.from(iv).map((char) => char.charCodeAt(0)),
    ),
  }
  const cryptoKey = await crypto.subtle.importKey(
    'raw',
    decode(key).buffer,
    alg,
    true,
    ['encrypt', 'decrypt'],
  )
  const cipherStr = atob(message).slice(12)
  const cipherBuf = new Uint8Array(
    Array.from(cipherStr).map((char) => char.charCodeAt(0)),
  )
  const buf = await crypto.subtle.decrypt(alg, cryptoKey, cipherBuf)

  return new TextDecoder().decode(buf)
}


================================================
FILE: deno.json
================================================
{
  "compilerOptions": {
    "jsxFactory": "h",
    "jsxFragmentFactory": "Fragment"
  },
  "fmt": {
    "semiColons": false,
    "singleQuote": true,
    "exclude": [
      "./changelog.md",
      "./readme.md"
    ]
  },
  "imports": {
    "authenticus": "https://deno.land/x/authenticus@v2.0.3/mod.ts",
    "authenticus/preset": "https://deno.land/x/authenticus@v2.0.3/createPreset.ts",
    "aws4fetch": "https://esm.sh/aws4fetch@1.0.17?target=es2022",
    "brotli": "https://deno.land/x/brotli@0.1.7/mod.ts",
    "cliffy": "https://deno.land/x/cliffy@v0.25.7/mod.ts",
    "cliffy/keypress": "https://deno.land/x/cliffy@v0.25.7/keypress/mod.ts",
    "djwt": "https://deno.land/x/djwt@v2.8/mod.ts",
    "dom": "https://deno.land/x/deno_dom@v0.1.40/deno-dom-wasm.ts",
    "esbuild": "https://deno.land/x/esbuild@v0.19.3/mod.js",
    "foras": "https://deno.land/x/foras@v2.1.1/src/deno/mod.ts",
    "otpauth": "https://deno.land/x/otpauth@v9.1.4/dist/otpauth.esm.js",
    "preact": "https://esm.sh/preact@10.17.1?target=es2022",
    "preact/render-to-string": "https://esm.sh/preact-render-to-string@6.1.0?deps=preact@10.17.1&target=es2022",
    "std/": "https://deno.land/std@0.203.0/",
    "twind": "https://esm.sh/@twind/core@1.1.3",
    "twind/preset-autoprefix": "https://esm.sh/@twind/preset-autoprefix@1.0.7",
    "twind/preset-tailwind": "https://esm.sh/@twind/preset-tailwind@1.1.4",
    "worker": "https://cdn.jsdelivr.net/npm/@cloudflare/workers-types@4.20230922.0/index.ts",
    "upstash": "https://deno.land/x/upstash_redis@v1.22.0/mod.ts",
    "zod": "https://deno.land/x/zod@v3.21.4/mod.ts"
  },
  "lock": false,
  "tasks": {
    "check": "deno fmt && deno lint --unstable && deno task test",
    "test": "deno test -A --unstable"
  }
}


================================================
FILE: env.d.ts
================================================
declare global {
  type Variables = Record<string, unknown>
}

export {}


================================================
FILE: ext/compress.ts
================================================
// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.
import { compress as brotli } from 'brotli'
import { deflate, gzip, initBundledOnce, InitOutput } from 'foras'
import { createExtension } from '../mod.ts'

type CompressionAlgorithm = {
  format: 'br' | 'deflate' | 'gzip'
  compress: (input: Uint8Array) => Uint8Array | Promise<Uint8Array>
}

let FORAS: InitOutput | undefined

export const BROTLI: CompressionAlgorithm = {
  format: 'br',
  compress: brotli,
}

export const DEFLATE: CompressionAlgorithm = {
  format: 'deflate',
  async compress(input) {
    if (!FORAS) {
      FORAS = await initBundledOnce()
    }

    return deflate(input)
  },
}

export const GZIP: CompressionAlgorithm = {
  format: 'gzip',
  async compress(input) {
    if (!FORAS) {
      FORAS = await initBundledOnce()
    }

    return gzip(input)
  },
}

/**
 * An extension to compress the body of the response with [brotli](https://github.com/google/brotli), [gzip](https://www.gzip.org), or [deflate](https://www.ietf.org/rfc/rfc1951.txt), based on the `Accept-Encoding` header of the incoming request.
 *
 * @since v1.0
 */
export const compress = createExtension<{
  algorithm: CompressionAlgorithm | CompressionAlgorithm[]
}>({
  async onResponse({ c, _ }) {
    const alg = _.algorithm instanceof Array ? _.algorithm : [_.algorithm]

    const header = c.req.headers['accept-encoding']

    if (c.res.body === null || !header || header === 'identity') {
      return
    }

    for (let i = 0; i < alg.length; ++i) {
      if (!header.includes(alg[i].format)) {
        continue
      }

      c.res.body = c.res.body instanceof Uint8Array
        ? await alg[i].compress(c.res.body)
        : c.res.body instanceof ArrayBuffer
        ? await alg[i].compress(new Uint8Array(c.res.body))
        : typeof c.res.body === 'string'
        ? await alg[i].compress(new TextEncoder().encode(c.res.body))
        : c.res.body

      if (c.res.body instanceof Uint8Array) {
        c.res.header('content-encoding', alg[i].format)
      }

      break
    }
  },
})


================================================
FILE: ext/debug.ts
================================================
// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.
import { format } from 'std/fmt/bytes.ts'
import {
  brightBlue,
  brightGreen,
  brightRed,
  gray,
  white,
} from 'std/fmt/colors.ts'
import { createExtension } from '../mod.ts'

/**
 * An extension to log every response (that didn't throw an exception).
 *
 * @since v1.0
 */
export const debug = createExtension({
  onResponse({ c }) {
    const code = c.res.code.toString()

    console.info(
      gray(
        `${
          code.startsWith('3')
            ? brightBlue(c.res.code.toString())
            : code.startsWith('2')
            ? brightGreen(c.res.code.toString())
            : brightRed(c.res.code.toString())
        } - ${c.req.method} ${white(new URL(c.req.raw.url).pathname)}${
          c.res.bodySize > -1 ? ` (${format(c.res.bodySize)})` : ''
        }`,
      ),
    )
  },
})


================================================
FILE: ext/favicon.ts
================================================
// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.
import { createExtension } from '../mod.ts'

let FAVICON: ArrayBuffer | Uint8Array | undefined

/**
 * An extension to set a neat [favicon](https://en.wikipedia.org/wiki/Favicon) for your app.
 *
 * The source for your favicon can be either a URL or a buffer.
 *
 * @since v1.1
 */
export const favicon = createExtension<{
  headers?: Record<string, string>
  source: string | ArrayBuffer | Uint8Array
}>({
  async onRequest({
    req,
    _: {
      headers,
      source,
    },
  }) {
    const parts = req.url.split('?')

    parts[0] = parts[0].slice(8)

    const pathname = parts[0].substring(parts[0].indexOf('/'))

    if (pathname !== '/favicon.ico') {
      return
    }

    if (!FAVICON) {
      if (typeof source === 'string') {
        const response = await fetch(source)

        FAVICON = await response.arrayBuffer()
      } else {
        FAVICON = source
      }
    }

    return new Response(FAVICON, {
      headers,
    })
  },
})


================================================
FILE: ext/files.ts
================================================
// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.
import { AwsClient } from 'aws4fetch'
import { join } from 'std/path/mod.ts'
import { R2Bucket } from 'worker'
import { createExtension } from '../extensions.ts'
import { AppContext } from '../mod.ts'

type GeneralOptions = {
  cacheControl?: string
  etag?: boolean
}

type FsOptions = {
  type?: 'fs'
  directory: string
}

type R2Options = {
  type: 'r2'
  name: string
}

type S3Options = {
  type: 's3'
  endpoint?: string
  accessKeyId?: string
  secretAccessKey?: string
}

const awsClient = new AwsClient({
  accessKeyId: '',
  secretAccessKey: '',
})

function getVar<T extends unknown = string | undefined>(
  app: AppContext,
  name: string,
): T {
  return app.runtime === 'cloudflare' && app.env
    ? app.env[name] as T
    : Deno.env.get(name) as T
}

/**
 * An extension to serve static files from Cloudflare R2, an S3 bucket, or the local file system.
 *
 * @copyright [@not-ivy](https://github.com/not-ivy), [@boywithkeyboard](https://github.com/boywithkeyboard)
 * @since v1.2
 */
export const files = createExtension<{
  serve: GeneralOptions & (FsOptions | R2Options | S3Options)
}>({
  // onPlugIn({ settings }) {
  //     if (settings.serve.type === 's3') {
  //     awsClient = new AwsClient({
  //       accessKeyId: settings.serve.accessKeyId,
  //       secretAccessKey: settings.serve.secretAccessKey,
  //     })
  //   }
  // },
  onRequest({
    app,
    prefix,
    _: {
      serve,
    },
    req: request,
  }) {
    switch (serve.type) {
      case 'r2':
        return handleR2Files(app, serve, prefix)
      case 's3': {
        const keyId = getVar(app, 'S3_ACCESS_KEY_ID') ??
          getVar(app, 's3_access_key_id') ?? serve.accessKeyId
        if (!keyId) throw new Error('S3_ACCESS_KEY_ID is not set')
        const accessKey = getVar(app, 'S3_SECRET_ACCESS_KEY') ??
          getVar(app, 's3_secret_access_key') ??
          serve.secretAccessKey
        if (!accessKey) throw new Error('S3_SECRET_ACCESS_KEY is not set')
        awsClient.accessKeyId = keyId
        awsClient.secretAccessKey = accessKey
        return handleS3Files(app, serve, prefix, request)
      }
      case 'fs':
      default:
        return handleFsFiles(app, serve, prefix)
    }
  },
})

async function handleS3Files(
  app: AppContext,
  serve: GeneralOptions & S3Options,
  prefix: string,
  request: Request,
) {
  const path = join(
    prefix !== '*'
      ? app.request.pathname.substring(prefix.length + 1)
      : app.request.pathname,
  )

  let response = await awsClient.fetch(
    `${serve.endpoint}${path}`,
    {
      headers: {
        ...(serve.etag !== false &&
          { etag: request.headers.get('if-none-match') ?? '' }),
        'cache-control': serve.cacheControl ?? 's-maxage=300', // 5m
      },
    },
  )

  if (response.status === 404) {
    const indexPath = join(path, 'index.html')
    response = await awsClient.fetch(
      `${serve.endpoint}${indexPath}`,
      {
        headers: {
          ...(serve.etag !== false &&
            { etag: request.headers.get('if-none-match') ?? '' }),
          'cache-control': serve.cacheControl ?? 's-maxage=300', // 5m
        },
      },
    )
    if (response.status === 404) {
      const indexPath = join(path, '404.html')
      response = await awsClient.fetch(
        `${serve.endpoint}${indexPath}`,
        {
          headers: {
            'cache-control': serve.cacheControl ?? 's-maxage=300', // 5m
          },
        },
      )
    }
  }

  return response.status === 404 ? undefined : response
}

async function handleR2Files(
  app: AppContext,
  serve: GeneralOptions & R2Options,
  prefix: string,
) {
  if (app.runtime !== 'cloudflare' || !app.env) {
    throw new Error(
      'You need to use the Cloudflare Workers runtime to serve static files from an R2 bucket!',
    )
  }

  const bucket = app.env[serve.name] as R2Bucket
  const path = prefix !== '*'
    ? app.request.pathname.substring(prefix.length + 1)
    : app.request.pathname

  let object = await bucket.get(path)
  if (object) {
    return new Response(object.body as ReadableStream, {
      headers: {
        ...(serve.etag !== false && { etag: object.httpEtag }),
        'cache-control': serve.cacheControl ?? 's-maxage=300', // 5m
      },
    })
  }

  const indexPath = join(app.request.pathname, 'index.html')
  object = await bucket.get(indexPath)
  if (object) {
    return new Response(object.body as ReadableStream, {
      headers: {
        ...(serve.etag !== false && { etag: object.httpEtag }),
        'cache-control': serve.cacheControl ?? 's-maxage=300', // 5m
      },
    })
  }

  const errorPath = join(prefix, '404.html')
  object = await bucket.get(errorPath)
  if (object) {
    return new Response(object.body as ReadableStream, {
      headers: {
        'cache-control': serve.cacheControl ?? 's-maxage=300', // 5m
      },
    })
  }
}

async function handleFsFiles(
  app: AppContext,
  serve: GeneralOptions & FsOptions,
  prefix: string,
) {
  const path = join(
    serve.directory,
    prefix !== '*'
      ? app.request.pathname.substring(prefix.length + 1)
      : app.request.pathname,
  )

  let stat: Deno.FileInfo
  let file: Deno.FsFile

  try {
    stat = await Deno.lstat(path)
    if (stat.isDirectory) {
      stat = await Deno.lstat(join(path, 'index.html'))
      file = await Deno.open(join(path, 'index.html'), { read: true })
    } else {
      file = await Deno.open(path, { read: true })
    }
  } catch {
    try {
      stat = await Deno.lstat(join(serve.directory, '404.html'))
      file = await Deno.open(join(serve.directory, '404.html'), { read: true })
    } catch {
      return
    }
  }

  return new Response(file.readable, {
    headers: {
      ...(serve.etag !== false && { etag: await etag(stat) }),
      'cache-control': serve.cacheControl ?? 's-maxage=300', // 5m
    },
  })
}

async function etag(stat: Deno.FileInfo) {
  const encoder = new TextEncoder()
  const data = encoder.encode(
    `${stat.birthtime?.getTime()}:${stat.mtime?.getTime()}:${stat.size}`,
  )
  const hash = await crypto.subtle.digest({ name: 'SHA-1' }, data)
  const hashArray = Array.from(new Uint8Array(hash))
  const hashHex = hashArray.map((b) => b.toString(16).padStart(2, '0')).join('')
  return hashHex
}


================================================
FILE: ext/firewall.ts
================================================
import { createExtension } from '../mod.ts'

let VPN_LIST: string[] = []
let DATACENTER_LIST: string[] = []
let LAST_UPDATE = 0

type FirewallOptions = {
  blockVPN?: boolean
  blockDatacenter?: boolean
  customRanges?: string[]
}

/**
 * An extension for blocking specific traffic to your app.
 *
 * @namespace ext
 * @since v1.4
 */
export const firewall = createExtension<FirewallOptions>({
  async onRequest({ _: opts, app }) {
    await checkUpdate()
    if (
      opts?.blockVPN && VPN_LIST.find((range) => isIpInRange(app.ip, range))
    ) {
      return new Response(null, { status: 403 })
    }
    if (
      opts?.blockDatacenter &&
      DATACENTER_LIST.find((range) => isIpInRange(app.ip, range))
    ) return new Response(null, { status: 403 })
    if (
      opts?.customRanges &&
      opts.customRanges.find((range) => isIpInRange(app.ip, range))
    ) return new Response(null, { status: 403 })
  },
})

async function checkUpdate() {
  if (Date.now() - LAST_UPDATE < 9e5 /* 15 minutes */) return
  VPN_LIST = (await (await fetch(
    'https://raw.githubusercontent.com/X4BNet/lists_vpn/main/output/vpn/ipv4.txt',
  )).text()).split('\n')
  DATACENTER_LIST = (await (await fetch(
    'https://raw.githubusercontent.com/X4BNet/lists_vpn/main/output/datacenter/ipv4.txt',
  )).text()).split('\n')
  LAST_UPDATE = Date.now()
}

function isIpInRange(ip: string, range: string) {
  const [rangeIp, rangeMask] = range.split('/')
  const rangeStart = ipToNumber(rangeIp) >>> 0
  const rangeEnd = rangeStart + ((1 << (32 - parseInt(rangeMask))) - 1)
  const numIp = ipToNumber(ip)
  return numIp >= rangeStart && numIp <= rangeEnd
}

function ipToNumber(ip: string) {
  return ip.split('.').reduce((acc, val) => (acc << 8) | parseInt(val), 0) >>> 0
}


================================================
FILE: ext/helmet.ts
================================================
// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.
import { createExtension } from '../mod.ts'

/**
 * An extension to attach important security headers to the response.
 *
 * @since v1.0
 */
export const helmet = createExtension<{
  /**
   * Set the `Content-Security-Policy` header with a strict security policy.
   *
   * @default
   * true
   */
  contentSecurityPolicy?: boolean

  /**
   * Set the `Cross-Origin-Embedder-Policy` header.
   *
   * @default
   * null
   */
  crossOriginEmbedderPolicy?:
    | 'require-corp'
    | 'unsafe-none'
    | 'credentialless'
    | null

  /**
   * Set the `Cross-Origin-Opener-Policy` header.
   *
   * @default
   * 'same-origin'
   */
  crossOriginOpenerPolicy?:
    | 'same-origin'
    | 'same-origin-allow-popups'
    | 'unsafe-none'
    | null

  /**
   * Set the `Cross-Origin-Resource-Policy` header.
   *
   * @default
   * 'same-origin'
   */
  crossOriginResourcePolicy?:
    | 'same-origin'
    | 'same-site'
    | 'cross-origin'
    | null

  /**
   * Enable [DNS Prefetching](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-DNS-Prefetch-Control) at the expense of your users' privacy.
   *
   * @default
   * false
   */
  dnsPrefetching?: boolean

  /**
   * Set the `X-Frame-Options` header to mitigate Clickjacking.
   *
   * @default
   * 'sameorigin'
   */
  noFraming?:
    | 'deny'
    | 'sameorigin'
    | null

  /**
   * Set the `Strict-Transport-Security` header, which indicates to browsers to prefer a secure HTTPS connection.
   *
   * @default
   * {
   *   maxAge: 31536000, // a year
   *   includeSubDomains: true
   * }
   */
  hsts?: {
    maxAge?: number
    preload?: boolean
    includeSubDomains?: boolean
  } | null

  /**
   * Set the `X-Content-Type-Options` header to `nosniff`. This mitigates Content Sniffing, which can cause security vulnerabilities.
   *
   * @default
   * true
   */
  noSniffing?: boolean

  /**
   * Set the `Origin-Agent-Cluster` header, which provides a mechanism to allow web applications to isolate their origins.
   *
   * @default
   * true
   */
  originAgentCluster?: boolean

  /**
   * Set the `X-Permitted-Cross-Domain-Policies` header, which tells some clients (mostly Adobe products) your domain's policy for loading cross-domain content.
   *
   * @default
   * 'none'
   */
  crossDomainPolicy?:
    | 'none'
    | 'master-only'
    | 'by-content-type'
    | 'all'
    | null

  /**
   * Set the `Referrer-Policy` header to control what information is set in the `Referer` header.
   *
   * @default
   * 'no-referrer'
   */
  referrerPolicy?:
    | 'no-referrer'
    | 'no-referrer-when-downgrade'
    | 'origin'
    | 'origin-when-cross-origin'
    | 'same-origin'
    | 'strict-origin'
    | 'strict-origin-when-cross-origin'
    | 'unsafe-url'
    | null
}>({
  onResponse({
    c,
    _: {
      contentSecurityPolicy = true,
      crossOriginEmbedderPolicy = null,
      crossOriginOpenerPolicy = 'same-origin',
      crossOriginResourcePolicy = 'same-origin',
      dnsPrefetching = false,
      noFraming = 'sameorigin',
      hsts = {
        maxAge: 31536000, // a year
        includeSubDomains: true,
      },
      noSniffing = true,
      originAgentCluster = true,
      crossDomainPolicy = 'none',
      referrerPolicy = 'no-referrer',
    } = {},
  }) {
    if (contentSecurityPolicy) {
      c.res.header(
        'content-security-policy',
        `default-src 'self'; base-uri 'self'; font-src 'self' https: data:; form-action 'self'; frame-ancestors 'self'; img-src 'self' data:; object-src 'none'; script-src 'self'; script-src-attr 'none'; style-src 'self' https: 'unsafe-inline'; upgrade-insecure-requests`,
      )
    }

    if (crossOriginEmbedderPolicy !== null) {
      c.res.header('cross-origin-embedder-policy', crossOriginEmbedderPolicy)
    }

    if (crossOriginOpenerPolicy !== null) {
      c.res.header('cross-origin-opener-policy', crossOriginOpenerPolicy)
    }

    if (crossOriginResourcePolicy !== null) {
      c.res.header('cross-origin-resource-policy', crossOriginResourcePolicy)
    }

    if (dnsPrefetching !== null) {
      c.res.header('x-dns-prefetch-control', dnsPrefetching ? 'on' : 'off')
    }

    if (noFraming !== null) {
      c.res.header('x-frame-options', noFraming.toUpperCase())
    }

    if (hsts !== null) {
      c.res.header(
        'strict-transport-security',
        Object.entries(hsts)
          .map(([key, value]) =>
            `${key.replace('max-age', 'maxAge')}${
              typeof value === 'boolean' ? '' : `=${value}`
            }`
          )
          .join('; '),
      )
    }

    if (noSniffing) {
      c.res.header('x-content-type-options', 'nosniff')
    }

    if (originAgentCluster) {
      c.res.header('origin-agent-cluster', '?1')
    }

    if (crossDomainPolicy !== null) {
      c.res.header('x-permitted-cross-domain-policies', crossDomainPolicy)
    }

    if (referrerPolicy) {
      c.res.header('referrer-policy', 'no-referrer')
    }
  },
})


================================================
FILE: ext/pretty.ts
================================================
// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.
import { createExtension } from '../mod.ts'

function sortObject(object: unknown) {
  if (typeof object != 'object' || object instanceof Array || object === null) {
    return object
  }

  const keys = Object.keys(object).sort()

  const newObject: Record<string, unknown> = {}

  for (let i = 0; i < keys.length; i++) {
    newObject[keys[i]] = sortObject(
      (object as Record<string, unknown>)[keys[i]],
    )
  }

  return newObject
}

/**
 * An extension to jazz up the response body by adding indentation and sorting the keys alphabetically.
 *
 * @since v1.1
 */
export const pretty = createExtension<{
  /**
   * Apply indentation to your response body. Set it to `0` to disable indentation.
   *
   * @default 2
   */
  indentSize?: number
  /**
   * Whether to sort the fields alphabetically.
   *
   * @default false
   */
  sort?: boolean
}>({
  onResponse({
    c,
    _: {
      indentSize = 2,
      sort = true,
    } = {},
  }) {
    let body = c.res.body

    if (body === null || body.constructor.name !== 'Object') {
      return
    }

    c.res.header('content-type', 'application/json; charset=utf-8')

    if (sort) {
      body = sortObject(body) as Record<string, unknown>
    }

    c.res.body = JSON.stringify(
      body,
      null,
      indentSize,
    )
  },
})


================================================
FILE: extensions.ts
================================================
// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.
import { ZodString, ZodUnion } from 'zod'
import { Method } from './base.ts'
import { Handler, ObjectType } from './handler.ts'
import { AppContext, Context } from './mod.ts'

type HasRequired<T> = Partial<T> extends T ? false : true

type Req = Response | void | undefined
type Res = void | undefined

type ExtensionContext = {
  /** The prefix of the routes to which this extension is assigned. */
  prefix: string
}

export type Extension<
  Config extends Record<string, unknown> | unknown = never,
> = {
  __config: Config | undefined
  onPlugIn: HasRequired<Config> extends true ? ((
      context: ExtensionContext & {
        env: AppContext['env']
        routes: AppContext['routes']
        runtime: AppContext['runtime']
        setRoute: <
          Pathname extends `/${string}`,
          // deno-lint-ignore no-explicit-any
          ValidatedBody extends ObjectType | ZodString | ZodUnion<any>,
          ValidatedCookies extends ObjectType,
          ValidatedHeaders extends ObjectType,
          ValidatedQuery extends ObjectType,
        >(
          method: Method,
          pathname: Pathname,
          ...handler: (
            | {
              body?: ValidatedBody
              cookies?: ValidatedCookies
              headers?: ValidatedHeaders
              query?: ValidatedQuery
              cors?: string
            }
            | Handler<
              Pathname,
              ValidatedBody,
              ValidatedCookies,
              ValidatedHeaders,
              ValidatedQuery
            >
          )[]
        ) => void | Promise<void>
        settings: Config
      },
    ) => void | Promise<void>)
    : ((
      context: ExtensionContext & {
        env: AppContext['env']
        routes: AppContext['routes']
        runtime: AppContext['runtime']
        setRoute: <
          Pathname extends `/${string}`,
          // deno-lint-ignore no-explicit-any
          ValidatedBody extends ObjectType | ZodString | ZodUnion<any>,
          ValidatedCookies extends ObjectType,
          ValidatedHeaders extends ObjectType,
          ValidatedQuery extends ObjectType,
        >(
          method: Method,
          pathname: Pathname,
          ...handlers: (
            | {
              body?: ValidatedBody
              cookies?: ValidatedCookies
              headers?: ValidatedHeaders
              query?: ValidatedQuery
              cors?: string
            }
            | Handler<
              Pathname,
              ValidatedBody,
              ValidatedCookies,
              ValidatedHeaders,
              ValidatedQuery
            >
          )[]
        ) => void
        settings?: Config
      },
    ) => void | Promise<void>)
  onRequest?: HasRequired<Config> extends true ? ((
      context: ExtensionContext & {
        app: AppContext
        req: Request
        _: Config
      },
    ) => Req | Promise<Req>)
    : ((
      context: ExtensionContext & {
        app: AppContext
        req: Request
        _?: Config
      },
    ) => Req | Promise<Req>)
  onResponse?: HasRequired<Config> extends true ? ((
      context: ExtensionContext & {
        app: AppContext
        c: Context
        _: Config
      },
    ) => Res | Promise<Res>)
    : ((
      context: ExtensionContext & {
        app: AppContext
        c: Context
        _?: Config
      },
    ) => Res | Promise<Res>)
}

type ReturnFunction<
  Config extends Record<string, unknown> | unknown = unknown,
> = Config extends Record<string, unknown>
  ? (HasRequired<Config> extends true ? ((config: Config) => Extension<Config>)
    : ((config?: Config) => Extension<Config>))
  : (() => Extension<Config>)

export function validExtension(ext: Record<string, unknown>) {
  const symbol = Object.getOwnPropertySymbols(ext).find((s) =>
    s.description === 'cheetah.extension'
  )

  // @ts-ignore:
  return symbol !== undefined && ext[symbol] === 'v1.0'
}

export function createExtension<
  Config extends Record<string, unknown> | unknown = unknown,
>({
  onPlugIn,
  onRequest,
  onResponse,
}: {
  onPlugIn?: Extension<Config>['onPlugIn']
  onRequest?: Extension<Config>['onRequest']
  onResponse?: Extension<Config>['onResponse']
}) {
  return ((__config?: Config) => {
    return {
      __config,
      onPlugIn,
      onRequest,
      onResponse,
      [Symbol('cheetah.extension')]: 'v1.0',
    }
  }) as ReturnFunction<Config>
}


================================================
FILE: handler.ts
================================================
// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.
import {
  ZodObject,
  ZodRecord,
  ZodString,
  ZodType,
  ZodTypeDef,
  ZodUnion,
} from 'zod'
import { Context } from './mod.ts'

export type ObjectType =
  // deno-lint-ignore no-explicit-any
  | ZodObject<any>
  | ZodRecord

export type BaseType<T extends ZodTypeDef = ZodTypeDef> =
  // deno-lint-ignore no-explicit-any
  ZodType<any, T, any>

type ExtractParam<Path, NextPart> = Path extends `:${infer Param}`
  ? Record<Param, string> & NextPart
  : NextPart

type ExtractParams<Path> = Path extends `${infer Segment}/${infer Rest}`
  ? ExtractParam<Segment, ExtractParams<Rest>>
  // deno-lint-ignore ban-types
  : ExtractParam<Path, {}>

export type Payload =
  | ArrayBuffer
  | Blob
  | FormData
  | ReadableStream<unknown>
  | Record<string, unknown>
  | Uint8Array
  | string
  | undefined
  | void

type Number = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9

export type Version = `v${
  | Exclude<Number, 0>
  | `${Exclude<Number, 0>}${Number}`}`

export type VersionRange =
  | Version // exact version
  | `> ${Version}` // greater than version
  | `< ${Version}` // smaller than version
  | `>= ${Version}` // greater than or equal to version
  | `<= ${Version}` // smaller than or equal to version
  | `${Version}...${Version}` // from (min) ... to (max)

export type Handler<
  Pathname extends `/${string}` | unknown,
  // deno-lint-ignore no-explicit-any
  ParsedBody extends ObjectType | ZodString | ZodUnion<any> = never,
  ParsedCookies extends ObjectType = never,
  ParsedHeaders extends ObjectType = never,
  ParsedQuery extends ObjectType = never,
> = (
  c: Context<
    ExtractParams<Pathname>,
    ParsedBody,
    ParsedCookies,
    ParsedHeaders,
    ParsedQuery
  >,
  next: () => void,
) => Payload | Promise<Payload>

export function handler<T>() {
  return <
    Pathname extends `/${string}`,
    // deno-lint-ignore no-explicit-any
    ValidatedBody extends ObjectType | ZodString | ZodUnion<any> = never,
    ValidatedCookies extends ObjectType = never,
    ValidatedHeaders extends ObjectType = never,
    ValidatedQuery extends ObjectType = never,
  >(
    // deno-lint-ignore no-unused-vars
    pathname: Pathname,
    // deno-lint-ignore no-unused-vars
    ...handler: (
      | ({
        body?: ValidatedBody
        cookies?: ValidatedCookies
        headers?: ValidatedHeaders
        query?: ValidatedQuery
        cors?: string
        params?: Partial<Record<keyof ExtractParams<Pathname>, ZodType>>
        gateway?: VersionRange
      })
      | Handler<
        Pathname,
        ValidatedBody,
        ValidatedCookies,
        ValidatedHeaders,
        ValidatedQuery
      >
    )[]
  ): T => {
    return null as T
  }
}

export type BodylessHandler<
  Pathname extends `/${string}` | unknown,
  // deno-lint-ignore no-explicit-any
  ParsedBody extends ObjectType | ZodString | ZodUnion<any> = never,
  ParsedCookies extends ObjectType = never,
  ParsedHeaders extends ObjectType = never,
  ParsedQuery extends ObjectType = never,
> = (
  c: Context<
    ExtractParams<Pathname>,
    ParsedBody,
    ParsedCookies,
    ParsedHeaders,
    ParsedQuery
  >,
  next: () => void,
) => Payload | Promise<Payload>

export function bodylessHandler<T>() {
  return <
    Pathname extends `/${string}`,
    ValidatedCookies extends ObjectType = never,
    ValidatedHeaders extends ObjectType = never,
    ValidatedQuery extends ObjectType = never,
  >(
    // deno-lint-ignore no-unused-vars
    pathname: Pathname,
    // deno-lint-ignore no-unused-vars
    ...handler: (
      | {
        cookies?: ValidatedCookies
        headers?: ValidatedHeaders
        query?: ValidatedQuery
        cors?: string
        params?: Partial<Record<keyof ExtractParams<Pathname>, ZodType>>
        gateway?: VersionRange
      }
      | BodylessHandler<
        Pathname,
        never,
        ValidatedCookies,
        ValidatedHeaders,
        ValidatedQuery
      >
    )[]
  ): T => {
    return null as T
  }
}

export type HandlerOrSchema =
  | {
    body?: ZodType
    cookies?: ObjectType
    headers?: ObjectType
    query?: ObjectType
    cors?: string
    params?: Record<string, ZodType>
    gateway?: VersionRange
  }
  | Handler<unknown>
  | BodylessHandler<unknown>


================================================
FILE: jwt.ts
================================================
// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.
import * as Jwt from 'djwt'
import { decode } from 'std/encoding/base64.ts'
import { Context } from './mod.ts'

interface Payload {
  iss?: string
  sub?: string
  aud?: string[] | string
  /**
   * A `Date` object or a `number` (in seconds) when the JWT will expire.
   */
  exp?: Date | number
  /**
   * A `Date` object or a `number` (in seconds) until which the JWT will be invalid.
   */
  nbf?: Date | number
  iat?: number
  jti?: string
  [key: string]: unknown
}

function importKey(key: string) {
  return crypto.subtle.importKey(
    'raw',
    decode(key).buffer,
    { name: 'HMAC', hash: 'SHA-512' },
    true,
    ['sign', 'verify'],
  )
}

/**
 * Sign a payload.
 */
// deno-lint-ignore ban-types
export async function sign<T extends Record<string, unknown> = {}>(
  secret: string | Context,
  payload: T & Payload,
) {
  const key = typeof secret === 'string'
    ? await importKey(secret)
    : await importKey(
      (secret.env('jwt_secret') ?? secret.env('JWT_SECRET')) as string,
    )

  const { exp, nbf, ...rest } = payload

  return await Jwt.create({ alg: 'HS512', typ: 'JWT' }, {
    ...(exp && { exp: Jwt.getNumericDate(exp) }),
    ...(nbf && { nbf: Jwt.getNumericDate(nbf) }),
    ...rest,
  }, key)
}

/**
 * Verify the validity of a JWT.
 */
export async function verify<T extends Record<string, unknown> = Payload>(
  secret: string | Context,
  token: string,
  options?: Jwt.VerifyOptions,
) {
  try {
    const key = typeof secret === 'string'
      ? await importKey(secret)
      : await importKey(
        (secret.env('jwt_secret') ?? secret.env('JWT_SECRET')) as string,
      )

    return await Jwt.verify(token, key, options) as Jwt.Payload & T
  } catch (_err) {
    return
  }
}

export default {
  sign,
  verify,
}


================================================
FILE: license
================================================
                                 Apache License
                           Version 2.0, January 2004
                        http://www.apache.org/licenses/

   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION

   1. Definitions.

      "License" shall mean the terms and conditions for use, reproduction,
      and distribution as defined by Sections 1 through 9 of this document.

      "Licensor" shall mean the copyright owner or entity authorized by
      the copyright owner that is granting the License.

      "Legal Entity" shall mean the union of the acting entity and all
      other entities that control, are controlled by, or are under common
      control with that entity. For the purposes of this definition,
      "control" means (i) the power, direct or indirect, to cause the
      direction or management of such entity, whether by contract or
      otherwise, or (ii) ownership of fifty percent (50%) or more of the
      outstanding shares, or (iii) beneficial ownership of such entity.

      "You" (or "Your") shall mean an individual or Legal Entity
      exercising permissions granted by this License.

      "Source" form shall mean the preferred form for making modifications,
      including but not limited to software source code, documentation
      source, and configuration files.

      "Object" form shall mean any form resulting from mechanical
      transformation or translation of a Source form, including but
      not limited to compiled object code, generated documentation,
      and conversions to other media types.

      "Work" shall mean the work of authorship, whether in Source or
      Object form, made available under the License, as indicated by a
      copyright notice that is included in or attached to the work
      (an example is provided in the Appendix below).

      "Derivative Works" shall mean any work, whether in Source or Object
      form, that is based on (or derived from) the Work and for which the
      editorial revisions, annotations, elaborations, or other modifications
      represent, as a whole, an original work of authorship. For the purposes
      of this License, Derivative Works shall not include works that remain
      separable from, or merely link (or bind by name) to the interfaces of,
      the Work and Derivative Works thereof.

      "Contribution" shall mean any work of authorship, including
      the original version of the Work and any modifications or additions
      to that Work or Derivative Works thereof, that is intentionally
      submitted to Licensor for inclusion in the Work by the copyright owner
      or by an individual or Legal Entity authorized to submit on behalf of
      the copyright owner. For the purposes of this definition, "submitted"
      means any form of electronic, verbal, or written communication sent
      to the Licensor or its representatives, including but not limited to
      communication on electronic mailing lists, source code control systems,
      and issue tracking systems that are managed by, or on behalf of, the
      Licensor for the purpose of discussing and improving the Work, but
      excluding communication that is conspicuously marked or otherwise
      designated in writing by the copyright owner as "Not a Contribution."

      "Contributor" shall mean Licensor and any individual or Legal Entity
      on behalf of whom a Contribution has been received by Licensor and
      subsequently incorporated within the Work.

   2. Grant of Copyright License. Subject to the terms and conditions of
      this License, each Contributor hereby grants to You a perpetual,
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
      copyright license to reproduce, prepare Derivative Works of,
      publicly display, publicly perform, sublicense, and distribute the
      Work and such Derivative Works in Source or Object form.

   3. Grant of Patent License. Subject to the terms and conditions of
      this License, each Contributor hereby grants to You a perpetual,
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
      (except as stated in this section) patent license to make, have made,
      use, offer to sell, sell, import, and otherwise transfer the Work,
      where such license applies only to those patent claims licensable
      by such Contributor that are necessarily infringed by their
      Contribution(s) alone or by combination of their Contribution(s)
      with the Work to which such Contribution(s) was submitted. If You
      institute patent litigation against any entity (including a
      cross-claim or counterclaim in a lawsuit) alleging that the Work
      or a Contribution incorporated within the Work constitutes direct
      or contributory patent infringement, then any patent licenses
      granted to You under this License for that Work shall terminate
      as of the date such litigation is filed.

   4. Redistribution. You may reproduce and distribute copies of the
      Work or Derivative Works thereof in any medium, with or without
      modifications, and in Source or Object form, provided that You
      meet the following conditions:

      (a) You must give any other recipients of the Work or
          Derivative Works a copy of this License; and

      (b) You must cause any modified files to carry prominent notices
          stating that You changed the files; and

      (c) You must retain, in the Source form of any Derivative Works
          that You distribute, all copyright, patent, trademark, and
          attribution notices from the Source form of the Work,
          excluding those notices that do not pertain to any part of
          the Derivative Works; and

      (d) If the Work includes a "NOTICE" text file as part of its
          distribution, then any Derivative Works that You distribute must
          include a readable copy of the attribution notices contained
          within such NOTICE file, excluding those notices that do not
          pertain to any part of the Derivative Works, in at least one
          of the following places: within a NOTICE text file distributed
          as part of the Derivative Works; within the Source form or
          documentation, if provided along with the Derivative Works; or,
          within a display generated by the Derivative Works, if and
          wherever such third-party notices normally appear. The contents
          of the NOTICE file are for informational purposes only and
          do not modify the License. You may add Your own attribution
          notices within Derivative Works that You distribute, alongside
          or as an addendum to the NOTICE text from the Work, provided
          that such additional attribution notices cannot be construed
          as modifying the License.

      You may add Your own copyright statement to Your modifications and
      may provide additional or different license terms and conditions
      for use, reproduction, or distribution of Your modifications, or
      for any such Derivative Works as a whole, provided Your use,
      reproduction, and distribution of the Work otherwise complies with
      the conditions stated in this License.

   5. Submission of Contributions. Unless You explicitly state otherwise,
      any Contribution intentionally submitted for inclusion in the Work
      by You to the Licensor shall be under the terms and conditions of
      this License, without any additional terms or conditions.
      Notwithstanding the above, nothing herein shall supersede or modify
      the terms of any separate license agreement you may have executed
      with Licensor regarding such Contributions.

   6. Trademarks. This License does not grant permission to use the trade
      names, trademarks, service marks, or product names of the Licensor,
      except as required for reasonable and customary use in describing the
      origin of the Work and reproducing the content of the NOTICE file.

   7. Disclaimer of Warranty. Unless required by applicable law or
      agreed to in writing, Licensor provides the Work (and each
      Contributor provides its Contributions) on an "AS IS" BASIS,
      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
      implied, including, without limitation, any warranties or conditions
      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
      PARTICULAR PURPOSE. You are solely responsible for determining the
      appropriateness of using or redistributing the Work and assume any
      risks associated with Your exercise of permissions under this License.

   8. Limitation of Liability. In no event and under no legal theory,
      whether in tort (including negligence), contract, or otherwise,
      unless required by applicable law (such as deliberate and grossly
      negligent acts) or agreed to in writing, shall any Contributor be
      liable to You for damages, including any direct, indirect, special,
      incidental, or consequential damages of any character arising as a
      result of this License or out of the use or inability to use the
      Work (including but not limited to damages for loss of goodwill,
      work stoppage, computer failure or malfunction, or any and all
      other commercial damages or losses), even if such Contributor
      has been advised of the possibility of such damages.

   9. Accepting Warranty or Additional Liability. While redistributing
      the Work or Derivative Works thereof, You may choose to offer,
      and charge a fee for, acceptance of support, warranty, indemnity,
      or other liability obligations and/or rights consistent with this
      License. However, in accepting such obligations, You may act only
      on Your own behalf and on Your sole responsibility, not on behalf
      of any other Contributor, and only if You agree to indemnify,
      defend, and hold each Contributor harmless for any liability
      incurred by, or claims asserted against, such Contributor by reason
      of your accepting any such warranty or additional liability.

   END OF TERMS AND CONDITIONS

   Copyright 2023 Samuel Kopp

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.


================================================
FILE: location_data.ts
================================================
// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.
import { IncomingRequestCfProperties } from 'worker'
import { Context } from './context.ts'

type CloudflareRequest = Request & {
  cf: IncomingRequestCfProperties
}

/**
 * Inspect the geolocation data of the incoming request.
 *
 * You must either deploy your app to [Cloudflare Workers](https://developers.cloudflare.com/workers/runtime-apis/request/#incomingrequestcfproperties) or use [Cloudflare as a proxy](https://developers.cloudflare.com/support/network/configuring-ip-geolocation/) to use the `LocationData` API.
 */
export class LocationData {
  #c: Context

  constructor(c: Context) {
    this.#c = c
  }

  /**
   * The city the request originated from.
   *
   * @example 'Austin'
   */
  get city() {
    const city = (this.#c.req.raw as CloudflareRequest).cf?.city

    if (!city && this.#c.__app.proxy === 'cloudflare') {
      return this.#c.req.headers['cf-ipcity']
    }

    return city
  }

  /**
   * If known, the ISO 3166-2 name for the first level region associated with the IP address of the incoming request.
   *
   * @example 'Texas'
   */
  get region() {
    return (this.#c.req.raw as CloudflareRequest).cf?.region
  }

  /**
   * The [ISO 3166-1 Alpha 2](https://www.iso.org/iso-3166-country-codes.html) country code the request originated from.
   *
   * If you're using CLoudflare Workers and your worker is [configured to accept TOR connections](https://support.cloudflare.com/hc/en-us/articles/203306930-Understanding-Cloudflare-Tor-support-and-Onion-Routing), this may also be `T1`, indicating a request that originated over TOR.
   *
   * If Cloudflare is unable to determine where the request originated this property is omitted.
   *
   * @example 'GB'
   */
  get country(): IncomingRequestCfProperties['country'] {
    const country = (this.#c.req.raw as CloudflareRequest).cf?.country

    if (!country && this.#c.__app.proxy === 'cloudflare') {
      return this.#c.req
        .headers['cf-ipcountry'] as IncomingRequestCfProperties['country']
    }

    return country
  }

  /**
   * A two-letter code indicating the continent the request originated from.
   *
   * @example 'NA'
   */
  get continent(): IncomingRequestCfProperties['continent'] {
    const continent = (this.#c.req.raw as CloudflareRequest).cf?.continent

    if (!continent && this.#c.__app.proxy === 'cloudflare') {
      return this.#c.req
        .headers['cf-ipcontinent'] as IncomingRequestCfProperties['continent']
    }

    return continent
  }

  /**
   * If known, the ISO 3166-2 code for the first-level region associated with the IP address of the incoming request.
   *
   * @example 'TX'
   */
  get regionCode(): IncomingRequestCfProperties['regionCode'] {
    return (this.#c.req.raw as CloudflareRequest).cf?.regionCode
  }

  /**
   * Latitude of the incoming request.
   *
   * @example '30.27130'
   */
  get latitude(): IncomingRequestCfProperties['latitude'] {
    const latitude = (this.#c.req.raw as CloudflareRequest).cf?.latitude

    if (!latitude && this.#c.__app.proxy === 'cloudflare') {
      return this.#c.req.headers['cf-iplatitude']
    }

    return latitude
  }

  /**
   * Longitude of the incoming request.
   *
   * @example '-97.74260'
   */
  get longitude(): IncomingRequestCfProperties['longitude'] {
    const longitude = (this.#c.req.raw as CloudflareRequest).cf?.longitude

    if (!longitude && this.#c.__app.proxy === 'cloudflare') {
      return this.#c.req.headers['cf-iplongitude']
    }

    return longitude
  }

  /**
   * Postal code of the incoming request.
   *
   * @example '78701'
   */
  get postalCode(): IncomingRequestCfProperties['postalCode'] {
    return (this.#c.req.raw as CloudflareRequest).cf?.postalCode
  }

  /**
   * Timezone of the incoming request.
   *
   * @example 'America/Chicago'
   */
  get timezone(): IncomingRequestCfProperties['timezone'] {
    return (this.#c.req.raw as CloudflareRequest).cf?.timezone
  }

  /**
   * The three-letter [IATA](https://en.wikipedia.org/wiki/IATA_airport_code) airport code of the data center that the request hit.
   *
   * @example 'DFW'
   */
  get datacenter(): IncomingRequestCfProperties['colo'] {
    return (this.#c.req.raw as CloudflareRequest).cf?.colo
  }
}


================================================
FILE: mod.ts
================================================
// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.
export { cheetah as default } from './cheetah.ts'
export type { AppConfig, AppContext } from './cheetah.ts'
export { Collection } from './collection.ts'
export { Context } from './context.ts'
export { decrypt, encrypt } from './crypto.ts'
export { createExtension } from './extensions.ts'
export type { Extension } from './extensions.ts'
export { default as jwt } from './jwt.ts'
export { LocationData } from './location_data.ts'
export { otp } from './otp.ts'
export { h, Renderer } from './render.ts'
export { sendMail } from './send_mail.ts'


================================================
FILE: oauth/client.ts
================================================
// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.
import { GitHub as githubPreset, Google as googlePreset } from 'authenticus'
import { Preset } from 'authenticus/preset'
import { OAuthMethod } from './types.ts'

export type OAuthClient = {
  name: OAuthMethod
  // deno-lint-ignore no-explicit-any
  preset: Preset<any, any, any>
}

/**
 * @since v1.3
 */
export const Google: OAuthClient = {
  name: 'google',
  preset: googlePreset,
}

/**
 * @since v1.3
 */
export const GitHub: OAuthClient = {
  name: 'github',
  preset: githubPreset,
}


================================================
FILE: oauth/get_session_data.ts
================================================
// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.
import { getCookies } from 'std/http/cookie.ts'
import { Context } from '../context.ts'
import { verify } from '../jwt.ts'
import { OAuthSessionData, OAuthSessionToken } from './types.ts'

/**
 * Get the data associated with the current session if logged in.
 *
 * @namespace oauth
 * @since v1.3
 */
export async function getSessionData(
  c: Context,
): Promise<OAuthSessionData | undefined> {
  if (!c.__app.oauth) {
    throw new Error('Please configure the oauth module for your app!')
  }

  const cookies = getCookies(c.req.raw.headers)

  if (!cookies.token) {
    return
  }

  const payload = await verify<OAuthSessionToken>(
    c,
    cookies.token,
    { audience: 'oauth:session' },
  )

  if (!payload) {
    return
  }

  if (payload.ip !== c.req.ip) {
    await c.__app.oauth.store.delete(c, payload.identifier)

    c.res.deleteCookie('token', {
      path: c.__app.oauth.cookie?.path ?? '/',
      ...(c.__app.oauth.cookie?.domain &&
        { domain: c.__app.oauth.cookie.domain }),
    })

    if (typeof c.__app.oauth.onSignOut === 'function') {
      await c.__app.oauth.onSignOut(c, payload.identifier)
    }

    return
  }

  const session = await c.__app.oauth.store.get(c, payload.identifier)

  return session
}


================================================
FILE: oauth/get_session_id.ts
================================================
// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.
import { getCookies } from 'std/http/cookie.ts'
import { Context } from '../context.ts'
import { verify } from '../jwt.ts'
import { OAuthSessionToken } from './types.ts'

/**
 * Get the session identifier and verify it.
 *
 * @namespace oauth
 * @since v1.3
 */
export async function getSessionId(c: Context): Promise<string | undefined> {
  if (!c.__app.oauth) {
    throw new Error('Please configure the oauth module for your app!')
  }

  const cookies = getCookies(c.req.raw.headers)

  if (!cookies.token) {
    return
  }

  const payload = await verify<OAuthSessionToken>(
    c,
    cookies.token,
    { audience: 'oauth:session' },
  )

  if (!payload) {
    return
  }

  if (payload.ip !== c.req.ip) {
    await c.__app.oauth.store.delete(c, payload.identifier)

    c.res.deleteCookie('token', {
      path: c.__app.oauth.cookie?.path ?? '/',
      ...(c.__app.oauth.cookie?.domain &&
        { domain: c.__app.oauth.cookie.domain }),
    })

    if (typeof c.__app.oauth.onSignOut === 'function') {
      await c.__app.oauth.onSignOut(c, payload.identifier)
    }

    return
  }

  if (await c.__app.oauth.store.has(c, payload.identifier)) {
    return payload.identifier
  }
}


================================================
FILE: oauth/get_session_token.ts
================================================
// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.
import { getCookies } from 'std/http/cookie.ts'
import { Context } from '../context.ts'
import { verify } from '../jwt.ts'

/**
 * Get the session token without verifying the session.
 *
 * @namespace oauth
 * @since v1.3
 */
export async function getSessionToken(c: Context) {
  const cookies = getCookies(c.req.raw.headers)

  if (!cookies.token) {
    return
  }

  const payload = await verify(
    c,
    cookies.token,
    { audience: 'oauth:session' },
  )

  if (payload) {
    return cookies.token
  }
}


================================================
FILE: oauth/handle_callback.ts
================================================
// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.
import { getNormalizedUser, getToken, getUser } from 'authenticus'
import { UserAgent } from 'std/http/user_agent.ts'
import { Context } from '../context.ts'
import { sign, verify } from '../jwt.ts'
import { LocationData } from '../location_data.ts'
import { OAuthClient } from './client.ts'
import {
  OAuthSessionData,
  OAuthSessionToken,
  OAuthSignInToken,
} from './types.ts'

export async function handleCallback(
  c: Context,
  client: OAuthClient,
) {
  if (!c.__app.oauth) {
    throw new Error('Please configure the oauth module for your app!')
  }

  // validate request

  if (
    typeof c.req.query.state !== 'string' ||
    typeof c.req.query.code !== 'string'
  ) {
    throw c.exception('Bad Request')
  }

  // validate state

  const payload = await verify<OAuthSignInToken>(
    c,
    c.req.query.state,
    { audience: 'oauth:sign_in' },
  )

  if (!payload || payload.ip !== c.req.ip) {
    throw c.exception('Access Denied')
  }

  try {
    // fetch user

    const { accessToken } = await getToken(client.preset, {
      clientId: (c.env(`${client.name.toUpperCase()}_CLIENT_ID`) ??
        c.env(`${client.name}_client_id`)) as string,
      clientSecret: (c.env(`${client.name.toUpperCase()}_CLIENT_SECRET`) ??
        c.env(`${client.name}_client_secret`)) as string,
      code: c.req.query.code,
      redirectUri: payload.redirectUri,
    })

    const user = getNormalizedUser(
      client.preset,
      // @ts-ignore:
      await getUser(client.preset, accessToken),
    )

    // create session

    const identifier = crypto.randomUUID()

    const expirationDate = new Date(Date.now() + 7 * 24 * 60 * 60000)

    const token = await sign<OAuthSessionToken>(
      c,
      {
        aud: 'oauth:session',
        exp: expirationDate,
        identifier,
        ip: c.req.ip,
      },
    )

    const userAgent = new UserAgent(c.req.headers['user-agent'] ?? '')

    const location = new LocationData(c)

    const data: OAuthSessionData = {
      identifier,
      email: user.email,
      method: client.name,
      userAgent: {
        browser: userAgent.browser,
        device: userAgent.device,
        os: userAgent.os,
      },
      location: {
        ip: c.req.ip,
        city: location.city,
        region: location.region,
        regionCode: location.regionCode,
        country: location.country,
        continent: location.continent,
      },
      expiresAt: expirationDate.getTime(),
    }

    c.__app.oauth.store.set(c, identifier, data, data.expiresAt)

    c.res.setCookie('token', token, {
      expires: expirationDate,
      httpOnly: true,
      secure: true,
      path: '/',
      ...c.__app.oauth.cookie,
    })

    if (typeof c.__app.oauth.onSignIn === 'function') {
      await c.__app.oauth.onSignIn(c, data)
    }

    return {
      token,
      ...data,
    }
  } catch (_err) {
    throw c.exception('Bad Request')
  }
}


================================================
FILE: oauth/is_signed_in.ts
================================================
// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.
import { Context } from '../context.ts'
import { getSessionId } from './get_session_id.ts'

/**
 * Check if the user is logged in.
 *
 * @namespace oauth
 * @since v1.3
 */
export async function isSignedIn(c: Context): Promise<boolean> {
  return await getSessionId(c) !== undefined
}


================================================
FILE: oauth/mod.ts
================================================
// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.
export { GitHub, Google } from './client.ts'
export type { OAuthClient } from './client.ts'
export { getSessionData } from './get_session_data.ts'
export { getSessionId } from './get_session_id.ts'
export { getSessionToken } from './get_session_token.ts'
export { handleCallback } from './handle_callback.ts'
export { isSignedIn } from './is_signed_in.ts'
export { signIn } from './sign_in.ts'
export { signOut } from './sign_out.ts'
export { kv, OAuthStore, upstash } from './store.ts'
export type { OAuthMethod, OAuthSessionData } from './types.ts'


================================================
FILE: oauth/sign_in.ts
================================================
// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.
import { createAuthorizeUrl } from 'authenticus'
import { Context } from '../context.ts'
import { sign } from '../jwt.ts'
import { OAuthClient } from './client.ts'
import { OAuthSignInToken } from './types.ts'

/**
 * Start the login flow by redirecting the user.
 *
 * @namespace oauth
 * @since v1.3
 */
export async function signIn(
  c: Context,
  client: OAuthClient,
  options: {
    redirectUri: string
    scopes?: string[]
  },
) {
  if (!c.__app.oauth) {
    throw new Error('Please configure the oauth module for your app!')
  }

  const token = await sign<OAuthSignInToken>(
    c,
    {
      aud: 'oauth:sign_in',
      exp: 300, // 5m
      ip: c.req.ip,
      redirectUri: options.redirectUri,
    },
  )

  const url = createAuthorizeUrl(client.preset, {
    clientId: c.env(`${client.name.toUpperCase()}_CLIENT_ID`) ??
      c.env(`${client.name}_client_id`),
    scopes: options.scopes,
    state: token,
    redirectUri: options.redirectUri,
  })

  c.res.redirect(url)
}


================================================
FILE: oauth/sign_out.ts
================================================
// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.
import { Context } from '../context.ts'
import { getSessionData } from './get_session_data.ts'

/**
 * Sign the user out if they're logged in.
 *
 * @namespace oauth
 * @since v1.3
 */
export async function signOut(c: Context) {
  if (!c.__app.oauth) {
    throw new Error('Please configure the oauth module for your app!')
  }

  c.res.deleteCookie('token', {
    path: c.__app.oauth.cookie?.path ?? '/',
    ...(c.__app.oauth.cookie?.domain &&
      { domain: c.__app.oauth.cookie.domain }),
  })

  const data = await getSessionData(c)

  if (!data) {
    return
  }

  try {
    await c.__app.oauth.store.delete(c, data.identifier)
  } catch (_err) {
    //
  }

  if (typeof c.__app.oauth.onSignOut === 'function') {
    await c.__app.oauth.onSignOut(c, data.identifier)
  }
}


================================================
FILE: oauth/store.ts
================================================
// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.
import { Redis } from 'upstash'
import { KVNamespace } from 'worker'
import { Context } from '../context.ts'
import { OAuthSessionData } from './types.ts'

export class OAuthStore {
  set: (
    c: Context,
    key: string,
    value: OAuthSessionData,
    expiresAt: number, // unix timestamp in ms
  ) => Promise<void>
  get: (c: Context, key: string) => Promise<OAuthSessionData | undefined>
  delete: (c: Context, key: string) => Promise<void>
  has: (c: Context, key: string) => Promise<boolean>

  constructor(
    options: {
      set: OAuthStore['set']
      get: OAuthStore['get']
      delete: OAuthStore['delete']
      has: OAuthStore['has']
    },
  ) {
    this.set = options.set
    this.get = options.get
    this.delete = options.delete
    this.has = options.has
  }
}

let KV: Deno.Kv | undefined

/**
 * Use [Cloudflare KV](https://developers.cloudflare.com/workers/runtime-apis/kv) or [Deno KV](https://deno.com/kv) as session storage, depending on the runtime.
 *
 * @namespace oauth
 * @since v1.3
 */
export const kv = new OAuthStore({
  async set(c, key, value, expiresAt) {
    if (c.runtime === 'cloudflare') {
      await (c.__app.env as { oauth: KVNamespace }).oauth.put(
        key,
        JSON.stringify(value),
        { expiration: Math.round(expiresAt / 1000) },
      )
    } else {
      if (!KV) {
        KV = await Deno.openKv('oauth')
      }

      await KV.set([key], value)
    }
  },

  async get(c, key) {
    if (c.runtime === 'cloudflare') {
      const kv = (c.__app.env as { oauth: KVNamespace }).oauth

      const value = await kv.get<OAuthSessionData>(key, 'json')

      if (value === null || value.expiresAt < Date.now()) {
        return
      }

      return value
    } else {
      if (!KV) {
        KV = await Deno.openKv('oauth')
      }

      const result = await KV.get<OAuthSessionData>([key], {
        consistency: 'strong',
      })

      if (result.value === null) {
        return
      }

      if (result.value.expiresAt > Date.now()) {
        return result.value
      }

      await KV.delete([key])
    }
  },

  async has(c, key) {
    if (c.runtime === 'cloudflare') {
      const kv = (c.__app.env as { oauth: KVNamespace }).oauth

      const value = await kv.get<OAuthSessionData>(key, 'json')

      if (value === null) {
        return false
      }

      return value.expiresAt > Date.now()
    } else {
      if (!KV) {
        KV = await Deno.openKv('oauth')
      }

      const result = await KV.get<OAuthSessionData>([key], {
        consistency: 'strong',
      })

      if (result.value === null) {
        return false
      }

      if (result.value.expiresAt > Date.now()) {
        return true
      }

      await KV.delete([key])

      return false
    }
  },

  async delete(c, key) {
    if (c.runtime === 'cloudflare') {
      const kv = (c.__app.env as { oauth: KVNamespace }).oauth

      await kv.delete(key)
    } else {
      if (!KV) {
        KV = await Deno.openKv('oauth')
      }

      await KV.delete([key])
    }
  },
})

let REDIS: Redis | undefined

/**
 * Use [Upstash](https://upstash.com) as session storage.
 *
 * @namespace oauth
 * @since v1.3
 */
export const upstash = new OAuthStore({
  async set(c, key, value, expiresAt) {
    if (!REDIS) {
      REDIS = new Redis({
        url: c.env('UPSTASH_URL') as string,
        token: c.env('UPSTASH_TOKEN') as string,
      })
    }

    await REDIS.set(key, value, {
      pxat: expiresAt,
    })
  },

  async get(c, key) {
    if (!REDIS) {
      REDIS = new Redis({
        url: c.env('UPSTASH_URL') as string,
        token: c.env('UPSTASH_TOKEN') as string,
      })
    }

    return await REDIS.get(key) ?? undefined
  },

  async has(c, key) {
    if (!REDIS) {
      REDIS = new Redis({
        url: c.env('UPSTASH_URL') as string,
        token: c.env('UPSTASH_TOKEN') as string,
      })
    }

    return await REDIS.exists(key) === 1
  },

  async delete(c, key) {
    if (!REDIS) {
      REDIS = new Redis({
        url: c.env('UPSTASH_URL') as string,
        token: c.env('UPSTASH_TOKEN') as string,
      })
    }

    await REDIS.del(key)
  },
})


================================================
FILE: oauth/types.ts
================================================
// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.
import { UserAgent } from 'std/http/user_agent.ts'
import { LocationData } from '../location_data.ts'

export type OAuthMethod =
  | 'google'
  | 'github'

export type OAuthSessionData = {
  identifier: string
  email: string
  method: OAuthMethod
  userAgent: {
    browser?: UserAgent['browser']
    device?: UserAgent['device']
    os?: UserAgent['os']
  }
  location: {
    ip: string
    city?: LocationData['city']
    region?: LocationData['region']
    regionCode?: LocationData['regionCode']
    country?: LocationData['country']
    continent?: LocationData['continent']
  }
  expiresAt: number
}

export type OAuthSessionToken = {
  aud: 'oauth:session'
  identifier: string
  ip: string
}

export type OAuthSignInToken = {
  aud: 'oauth:sign_in'
  ip: string
  redirectUri: string
}


================================================
FILE: otp.ts
================================================
// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.
import * as OTP from 'otpauth'

export const otp = {
  /**
   * Create a random secret.
   */
  secret(length = 64) {
    return [...Array(length)].map(() =>
      Math.floor(Math.random() * 16).toString(16)
    ).join('')
  },

  /**
   * Get the 6-digit token for a given timestamp.
   */
  token(secret: string, timestamp?: number) {
    const totp = new OTP.TOTP({
      algorithm: 'SHA1',
      digits: 6,
      period: 30,
      secret: OTP.Secret.fromHex(secret),
    })

    return totp.generate({ timestamp })
  },

  /**
   * Create a URI that you can use, for example, for a QR code to scan with Google Authenticator.
   */
  uri(label: string, issuer: string, secret: string) {
    const totp = new OTP.TOTP({
      issuer,
      label,
      algorithm: 'SHA1',
      digits: 6,
      period: 30,
      secret: OTP.Secret.fromHex(secret),
    })

    return totp.toString()
  },

  /**
   * Determine if a given token is valid.
   */
  validate(token: string, secret: string) {
    const totp = new OTP.TOTP({
      algorithm: 'SHA1',
      digits: 6,
      period: 30,
      secret: OTP.Secret.fromHex(secret),
    })

    return totp.validate({ token }) === 0
  },
}


================================================
FILE: readme.md
================================================
<div align='center'>
  <img src='https://cheetah.mod.land/cheetah.svg' width='128px' />
  <br>
  <br>
  <h1>cheetah</h1>
</div>

<div align='center'>
  <p><code>🛡️ secure</code> × <code>💎 simple</code> × <code>🪶 light</code></p>
</div>

<br>

> [!WARNING]
> cheetah is currently **not maintained**.

<br>

### Sneak Peek 👾

```ts
import cheetah from 'https://deno.land/x/cheetah/mod.ts'
import { z } from 'https://deno.land/x/zod/mod.ts'

const app = new cheetah()
  .post('/', {
    body: z.object({ // < scheme validation
      name: z.string()
    })
  }, async c => {
    const body = await c.req.body()

    return `Hey, ${body.name}!` // < response body
  })

app.serve() // < launch app
```

❔ Please read our [guide](https://cheetah.mod.land) or [join our Discord](https://discord.gg/2rCya9EWGv) to learn more.

<br>

---

<div align='center'>
  <sup>A big thank you goes to</sup>

  <br>
  <br>
  <br>

  <a href='https://deco.cx'>
    <img src='https://github.com/azurystudio/cheetah/blob/dev/.github/sponsors/deco.svg?raw=true' height='48px' />
    <br>
    <br>
    <a href='https://deco.cx'><sup><b>Build fast stores and increase sales.</b></sup></a>
  </a>
</div>

---

<br>

### Release Schedule 🗓️

We strictly adhere to [SemVer](https://semver.org) and post updates **weekly**.

- ◆ **current** *(e.g. v0.1.0)*

  The current channel is dedicated to stable releases and is safe for use in production.

- ◇ **canary** *(e.g. v0.1.0-canary.0)*

  The canary channel is meant for pre-releases that lack features for a stable release or contain features that are still a prototype. These releases are **not suited for production** and only meant for testing purposes.

<br>

### Contributing 😘

We appreciate your help! 💕

To contribute, please read our [contributing guidelines](https://github.com/azurystudio/cheetah/blob/dev/contributing.md)
first.


================================================
FILE: render.ts
================================================
// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.
import { VNode } from 'preact'
import renderToString from 'preact/render-to-string'
import { brightYellow, gray } from 'std/fmt/colors.ts'
import { defineConfig, extract, install } from 'twind'
import presetAutoPrefix from 'twind/preset-autoprefix'
import presetTailwind from 'twind/preset-tailwind'
import { Context } from './mod.ts'

export function render(c: Context, Component: VNode) {
  const htmlString = renderToString(Component)
  try {
    const { html, css } = extract(htmlString)
    c.res.body = `${css.length > 0 ? `<style>${css}</style>` : ''}${html}`
  } catch (_err) {
    if (c.dev) {
      console.warn(
        gray(
          `${
            brightYellow('warning')
          } - twind is not installed, thus styles might not be applied`,
        ),
      )
    }
    c.res.body = htmlString
  }
  c.res.header('content-type', 'text/html; charset=utf-8')
}

export class Renderer {
  render

  constructor(options?: Parameters<typeof defineConfig>[0]) {
    if (options) {
      options.presets = options.presets
        ? [presetAutoPrefix(), presetTailwind(), ...options.presets]
        : [presetAutoPrefix(), presetTailwind()]
    }

    install(defineConfig(
      options ?? {
        presets: [presetAutoPrefix(), presetTailwind()],
      },
    ))

    this.render = render
  }
}

export { h } from 'https://esm.sh/preact@10.17.1?target=es2022'


================================================
FILE: request_context.ts
================================================
// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.
import {
  deadline as resolveWithDeadline,
  DeadlineError,
} from 'std/async/deadline.ts'
import { z, ZodStringDef, ZodType, ZodUnionDef } from 'zod'
import { Method } from './base.ts'
import { BaseType, ObjectType } from './handler.ts'
import { AppContext, Context } from './mod.ts'

type Static<T extends ZodType> = T extends ZodType ? z.infer<T>
  : never

export class RequestContext<
  Params extends Record<string, unknown> = Record<string, never>,
  ValidatedBody extends ZodType = never,
  ValidatedCookies extends ObjectType = never,
  ValidatedHeaders extends ObjectType = never,
  ValidatedQuery extends ObjectType = never,
> {
  #c: Record<string, string | undefined> | undefined
  #h: Record<string, string | undefined> | undefined
  #a: AppContext
  #p
  #q: Record<string, unknown> | undefined
  #r
  #s
  #e

  constructor(
    a: AppContext,
    p: Record<string, string | undefined>,
    r: Request,
    s: {
      body?: ZodType | undefined
      cookies?: ObjectType | undefined
      headers?: ObjectType | undefined
      query?: ObjectType | undefined
      params?: Record<string, ZodType>
      [key: string]: unknown
    } | null,
    e: Context['exception'],
  ) {
    this.#a = a
    this.#p = p
    this.#r = r
    this.#s = s
    this.#e = e
  }

  get gateway(): number {
    return this.#a.gateway ?? -1
  }

  get ip(): string {
    return this.#a.ip
  }

  /**
   * The method of the incoming request.
   *
   * @example 'GET'
   * @since v0.12
   */
  get method() {
    return this.#r.method as Uppercase<Method>
  }

  /**
   * A method to retrieve the corresponding value of a parameter.
   */
  param<T extends keyof Params>(name: T): Params[T] {
    if (this.#s?.params && this.#s.params[name as string]) {
      const result = this.#s.params[name as string].safeParse(this.#p[name])

      if (!result.success) {
        throw this.#e('Bad Request')
      }

      return result.data as Params[T]
    } else {
      return this.#p[name as string] as Params[T]
    }
  }

  /**
   * Retrieve the original request object.
   *
   * @since v1.0
   */
  get raw() {
    return this.#r
  }

  /**
   * The validated body of the incoming request.
   */
  async body(options?: {
    /**
     * This enables the conversion of a FormData request body into a JSON object (if the request body has the MIME type `multipart/form-data`).
     *
     * @default false
     */
    transform: boolean
  }): Promise<
    [ValidatedBody] extends [never] ? unknown : Static<ValidatedBody>
  > {
    if (!this.#s?.body) {
      // @ts-ignore:
      return undefined
    }

    let body

    try {
      if (
        (this.#s.body as BaseType<ZodStringDef>)._def.typeName ===
          'ZodString' ||
        (this.#s.body as BaseType<ZodUnionDef>)._def.typeName === 'ZodUnion' &&
          (this.#s.body as BaseType<ZodUnionDef>)._def.options.every((
            { _def },
          ) => _def.typeName === 'ZodString')
      ) {
        body = await resolveWithDeadline(this.#r.text(), 2500)
      } else {
        if (
          (options?.transform === true || this.#s?.transform === true) &&
          this.#r.headers.get('content-type') === 'multipart/form-data'
        ) {
          const formData = await resolveWithDeadline(this.#r.formData(), 2500)

          body = {} as Record<string, unknown>

          for (const [key, value] of formData.entries()) {
            body[key] = value
          }
        } else {
          body = await resolveWithDeadline(this.#r.json(), 2500)
        }
      }
    } catch (err: unknown) {
      throw this.#e(
        err instanceof DeadlineError ? 'Content Too Large' : 'Bad Request',
      )
    }

    const result = this.#s.body.safeParse(body)

    if (!result.success) {
      throw this.#e('Bad Request')
    }

    return result.data
  }

  /**
   * The validated cookies of the incoming request.
   */
  get cookies(): [ValidatedCookies] extends [never] ? never
    : Static<ValidatedCookies> {
    if (this.#c || !this.#s?.cookies) {
      return this.#c as [ValidatedCookies] extends [never] ? never
        : Static<ValidatedCookies>
    }

    try {
      const header = this.#r.headers.get('cookies') ?? ''

      if (header.length > 1000) {
        throw this.#e('Content Too Large')
      }

      this.#c = header
        .split(/;\s*/)
        .map((pair) => pair.split(/=(.+)/))
        .reduce((acc: Record<string, string>, [k, v]) => {
          acc[k] = v

          return acc
        }, {})

      delete this.#c['']
    } catch (_err) {
      this.#c = {}
    }

    const isValid = this.#s.cookies.safeParse(this.#c).success

    if (!isValid) {
      throw this.#e('Bad Request')
    }

    return this.#c as [ValidatedCookies] extends [never] ? never
      : Static<ValidatedCookies>
  }

  /**
   * The validated headers of the incoming request.
   */
  get headers(): [ValidatedHeaders] extends [never]
    ? Record<string, string | undefined>
    : Static<ValidatedHeaders> {
    if (this.#h) {
      return this.#h as [ValidatedHeaders] extends [never]
        ? Record<string, string | undefined>
        : Static<ValidatedHeaders>
    }

    this.#h = {}

    let num = 0

    for (const [key, value] of this.#r.headers) {
      if (num === 50) {
        break
      }

      if (!this.#h[key.toLowerCase()]) {
        this.#h[key.toLowerCase()] = value
      }

      num++
    }

    if (this.#s?.headers) {
      const isValid = this.#s.headers.safeParse(this.#h).success

      if (!isValid) {
        throw this.#e('Bad Request')
      }
    }

    return this.#h as [ValidatedHeaders] extends [never]
      ? Record<string, string | undefined>
      : Static<ValidatedHeaders>
  }

  /**
   * The validated query parameters of the incoming request.
   */
  get query(): [ValidatedQuery] extends [never] ? Record<string, unknown>
    : Static<ValidatedQuery> {
    if (this.#q) {
      return this.#q as [ValidatedQuery] extends [never]
        ? Record<string, unknown>
        : Static<ValidatedQuery>
    }

    this.#q = {}

    if (this.#a.request.querystring) {
      const arr = this.#a.request.querystring.split('&')

      for (let i = 0; i < arr.length; i++) {
        const [key, value] = arr[i].split('=')

        if (!key) {
          continue
        }

        if (typeof value === 'undefined') {
          this.#q[key] = true

          continue
        }

        try {
          this.#q[key] = JSON.parse(decodeURIComponent(value))
        } catch (_err) {
          this.#q[key] = decodeURIComponent(value)
        }
      }
    }

    if (this.#s?.query) {
      const isValid = this.#s.query.safeParse(this.#q).success

      if (!isValid) {
        throw this.#e('Bad Request')
      }
    }

    return this.#q as [ValidatedQuery] extends [never] ? Record<string, unknown>
      : Static<ValidatedQuery>
  }

  /**
   * Parse the request body as an `ArrayBuffer` with a set time limit in ms.
   *
   * @param deadline (default `2500`)
   */
  async blob(deadline = 2500) {
    try {
      const promise = this.#r.blob()

      return await resolveWithDeadline(promise, deadline)
    } catch (_err) {
      return null
    }
  }

  /**
   * Parse the request body as an `ArrayBuffer` with a set time limit in ms.
   *
   * @param deadline (default `2500`)
   */
  async buffer(deadline = 2500) {
    try {
      const promise = this.#r.arrayBuffer()

      return await resolveWithDeadline(promise, deadline)
    } catch (_err) {
      return null
    }
  }

  /**
   * Parse the request body as JSON with a set time limit in ms.
   *
   * **If you have defined a validation schema, use `c.req.body()` instead!**
   *
   * @param deadline (default `2500`)
   */
  async json(deadline = 2500): Promise<unknown> {
    try {
      const promise = this.#r.json()

      return await resolveWithDeadline(promise, deadline)
    } catch (_err) {
      return null
    }
  }

  /**
   * Parse the request body as a `FormData` object with a set time limit in ms.
   *
   * @param deadline (default `2500`)
   */
  async formData(deadline = 2500) {
    try {
      const promise = this.#r.formData()

      return await resolveWithDeadline(promise, deadline)
    } catch (_err) {
      return null
    }
  }

  /**
   * Parse the request body as a `string` with a set time limit in ms.
   *
   * **If you have defined a validation schema, use `c.req.body()` instead!**
   *
   * @param deadline (default `2500`)
   */
  async text(deadline = 2500) {
    try {
      const promise = this.#r.text()

      return await resolveWithDeadline(promise, deadline)
    } catch (_err) {
      return null
    }
  }

  /**
   * A readable stream of the request body.
   */
  get stream() {
    return this.#r.body
  }
}


================================================
FILE: response_context.ts
================================================
// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.
import { Payload } from './handler.ts'

export class ResponseContext {
  #i

  constructor(__internal: {
    b: Exclude<Payload, void> | null
    c: number
    h: Headers
  }) {
    this.#i = __internal
  }

  get body(): Exclude<Payload, void> | null {
    return this.#i.b
  }

  set body(data: Exclude<Payload, void> | null) {
    this.#i.b = data
  }

  /**
   * The size of the response body (`-1` if it cannot be calculated).
   */
  get bodySize() {
    if (
      this.#i.b === null
    ) {
      return 0
    }

    let s

    switch (this.#i.b.constructor.name) {
      case 'String': {
        s = (this.#i.b as string).length
        break
      }

      case 'Object': {
        s = JSON.stringify(this.#i.b).length
        break
      }

      case 'ArrayBuffer': {
        s = (this.#i.b as ArrayBuffer).byteLength
        break
      }

      case 'Uint8Array': {
        s = (this.#i.b as ArrayBuffer).byteLength
        break
      }

      case 'Blob': {
        s = (this.#i.b as Blob).size
        break
      }

      default: {
        s = -1
        break
      }
    }

    return s
  }

  /**
   * The status code of the response.
   */
  get code() {
    return this.#i.c
  }

  set code(code: number) {
    this.#i.c = code
  }

  /**
   * Attach a cookie to the response.
   *
   * @since v1.4
   */
  setCookie(name: string, value: string, options?: {
    expires?: Date
    maxAge?: number
    domain?: string
    path?: string
    secure?: boolean
    httpOnly?: boolean
    sameSite?:
      | 'strict'
      | 'lax'
      | 'none'
  }) {
    let cookie = `${name}=${value};`

    this.#i.h.append(
      'Set-Cookie',
      (
        options?.expires &&
        (cookie += ` Expires=${options.expires.toUTCString()};`),
          options?.maxAge && (cookie += ` Max-Age=${options.maxAge};`),
          options?.domain && (cookie += ` Domain=${options.domain};`),
          options?.path && (cookie += ` Path=${options.path};`),
          options?.secure && (cookie += ' Secure;'),
          options?.httpOnly && (cookie += ' HttpOnly;'),
          options?.sameSite &&
          (cookie += ` SameSite=${
            options.sameSite.charAt(0).toUpperCase() +
            options.sameSite.slice(1)
          };`),
          cookie
      ),
    )
  }

  /**
   * Set an empty `Set-Cookie` header to delete the cookie.
   *
   * @since v1.4
   */
  deleteCookie(name: string, options?: { path?: string; domain?: string }) {
    this.setCookie(name, '', {
      expires: new Date(0),
      ...options,
    })
  }

  /**
   * Attach a header to the response.
   */
  header(name: string, value: string | undefined) {
    if (value === undefined) {
      this.#i.h.delete(name)
    } else {
      this.#i.h.set(name, value)
    }
  }

  /**
   * Redirect the incoming request.
   *
   * @param destination e.g. https://google.com
   * @param code e.g. 301, default 307
   */
  redirect(destination: string, code = 307) {
    this.#i.c = code
    this.#i.h.set('location', destination)
  }
}


================================================
FILE: send_mail.ts
================================================
// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.
type MailContact =
  | { name?: string; email: string }
  | { name?: string; email: string }[]
  | string
  | string[]

/**
 * Send an email through [mailchannels](https://blog.cloudflare.com/sending-email-from-workers-with-mailchannels).
 */
export function sendMail(
  options: {
    subject: string
    message: string
    from: {
      name: string
      email: string
    }
    to: MailContact
    cc?: MailContact
    bcc?: MailContact
    reply?: boolean
    dkim?: {
      domain?: string
      privateKey?: string
      selector?: string
    }
  },
) {
  // to
  const to: { name?: string; email: string }[] = []

  if (typeof options.to === 'string') {
    to.push({ email: options.to })
  } else if (options.to instanceof Array) {
    for (const recipient of options.to) {
      to.push(typeof recipient === 'string' ? { email: recipient } : recipient)
    }
  } else {
    to.push(options.to)
  }

  // cc
  const cc: { name?: string; email: string }[] = []

  if (typeof options.cc === 'string') {
    cc.push({ email: options.cc })
  } else if (options.cc instanceof Array) {
    for (const recipient of options.cc) {
      cc.push(typeof recipient === 'string' ? { email: recipient } : recipient)
    }
  } else if (options.cc) {
    cc.push(options.cc)
  }

  // bcc
  const bcc: { name?: string; email: string }[] = []

  if (typeof options.bcc === 'string') {
    bcc.push({ email: options.bcc })
  } else if (options.bcc instanceof Array) {
    for (const recipient of options.bcc) {
      bcc.push(
        typeof recipient === 'string' ? { email: recipient } : recipient,
      )
    }
  } else if (options.bcc) {
    bcc.push(options.bcc)
  }

  return fetch('https://api.mailchannels.net/tx/v1/send', {
    method: 'POST',
    headers: {
      'content-type': 'application/json',
    },
    body: JSON.stringify({
      personalizations: [{
        to,
        ...(cc.length > 0 && { cc }),
        ...(bcc.length > 0 && { bcc }),
        ...(options.dkim?.domain && { dkim_domain: options.to }),
        ...(options.dkim?.privateKey && { dkim_private_key: options.to }),
        ...(options.dkim?.selector && { dkim_selector: options.to }),
      }],
      from: options.from,
      subject: options.subject,
      content: [{
        type: options.message.startsWith('<html>') ? 'text/html' : 'text/plain',
        value: options.message,
      }],
      ...(options.reply && { reply_to: to[0] }),
    }),
  })
}


================================================
FILE: test/context.test.ts
================================================
// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.
import { assertEquals } from 'std/assert/mod.ts'
import cheetah from '../mod.ts'

Deno.test('Context', async (t) => {
  Deno.env.set('cheetah_test', 'test')

  const app = new cheetah()

  await t.step('c.runtime', async () => {
    app.get('/runtime', (c) => c.runtime)
    assertEquals(
      await (await app.fetch(new Request('http://localhost/runtime')))
        .text(),
      'deno',
    )
  })
})


================================================
FILE: test/cors.test.ts
================================================
// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.
import { assertEquals } from 'std/assert/mod.ts'
import cheetah from '../mod.ts'

Deno.test('CORS', async (t) => {
  const app = new cheetah({
    cors: '*',
  })

  await t.step('Global CORS', async () => {
    app.get('/global', () => 'test')

    const result = await app.fetch(
      new Request('http://localhost/global', {
        method: 'OPTIONS',
        headers: {
          origin: 'custom.com',
          'access-control-request-method': 'GET',
        },
      }),
    )

    assertEquals(result.headers.get('access-control-allow-origin'), '*')
  })

  await t.step('Per-Route CORS', async () => {
    app.get('/per-route', { cors: 'custom.com' }, () => 'test')

    const result = await app.fetch(
      new Request('http://localhost/per-route', {
        method: 'OPTIONS',
        headers: {
          origin: 'foobar.com',
          'access-control-request-method': 'GET',
        },
      }),
    )

    assertEquals(
      result.headers.get('access-control-allow-origin'),
      'custom.com',
    )
  })
})


================================================
FILE: test/exception.test.ts
================================================
// TODO create test suite for c.exception


================================================
FILE: test/ext/favicon.test.ts
================================================
// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.
import { assertEquals } from 'std/assert/mod.ts'
import { favicon } from '../../ext/favicon.ts'
import cheetah from '../../mod.ts'

Deno.test('ext/favicon', async () => {
  const app = new cheetah()

  app.use(favicon({
    source: await Deno.readFile('./.github/cheetah.svg'),
  }))

  app.get('/', () => 'Hello World')

  const res = await app.fetch(new Request('http://localhost/favicon.ico'))

  assertEquals(
    await res.text(),
    await Deno.readTextFile('./.github/cheetah.svg'),
  )

  assertEquals(
    res.status,
    200,
  )
})


================================================
FILE: test/ext/files.test.ts
================================================
// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.
import { assertEquals } from 'std/assert/mod.ts'
import { files } from '../../ext/files.ts'
import cheetah from '../../mod.ts'

Deno.test('ext/files', async (t) => {
  await t.step('root', async () => {
    const app = new cheetah()

    app.use(files({
      serve: {
        directory: './test',
      },
    }))

    app.get('/', () => 'Hello World')

    const res1 = await app.fetch(new Request('http://localhost'))

    assertEquals(
      await res1.text(),
      'Hello World',
    )

    assertEquals(
      res1.status,
      200,
    )

    const res2 = await app.fetch(
      new Request('http://localhost/render.test.tsx'),
    )

    assertEquals(
      await res2.text(),
      await Deno.readTextFile('./test/render.test.tsx'),
    )

    assertEquals(
      res2.status,
      200,
    )
  })

  await t.step('with prefix', async () => {
    const app = new cheetah()

    app.use(
      '/t',
      files({
        serve: {
          directory: './test',
        },
      }),
    )

    app.get('/', () => 'Hello World')

    const res1 = await app.fetch(new Request('http://localhost'))

    assertEquals(
      await res1.text(),
      'Hello World',
    )

    assertEquals(
      res1.status,
      200,
    )

    const res2 = await app.fetch(
      new Request('http://localhost/t/ext/pretty.test.ts'),
    )

    assertEquals(
      await res2.text(),
      await Deno.readTextFile('./test/ext/pretty.test.ts'),
    )

    assertEquals(
      res2.status,
      200,
    )
  })
})


================================================
FILE: test/ext/firewall.test.ts
================================================
import { assertEquals } from 'std/assert/mod.ts'
import { firewall } from '../../ext/firewall.ts'
import cheetah from '../../mod.ts'

Deno.test('ext/firewall', async (t) => {
  await t.step('vpn', async () => {
    const app = new cheetah({ proxy: 'cloudflare' })

    app.use(firewall({
      blockVPN: true,
    }))

    const res = await app.fetch(
      new Request('http://localhost', {
        headers: {
          'cf-connecting-ip': '2.56.16.0',
        },
      }),
    )

    assertEquals(res.status, 403)
  })
})

Deno.test('datacenters', async () => {
  const app = new cheetah({ proxy: 'cloudflare' })

  app.use(firewall({
    blockDatacenter: true,
  }))

  const res = await app.fetch(
    new Request('http://localhost', {
      headers: {
        'cf-connecting-ip': '1.12.32.0',
      },
    }),
  )

  assertEquals(res.status, 403)
})

Deno.test('customRanges', async () => {
  const app = new cheetah({ proxy: 'cloudflare' })

  app.use(firewall({
    customRanges: [
      '1.2.3.4/32',
    ],
  }))

  const res = await app.fetch(
    new Request('http://localhost', {
      headers: {
        'cf-connecting-ip': '1.2.3.4',
      },
    }),
  )

  assertEquals(res.status, 403)
})


================================================
FILE: test/ext/pretty.test.ts
================================================
// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.
import { assertEquals } from 'std/assert/mod.ts'
import { pretty } from '../../ext/pretty.ts'
import cheetah from '../../mod.ts'

Deno.test('ext/pretty', async (t) => {
  const obj = {
    lastname: 'Doe',
    firstname: 'John',
    data: {
      height: 180,
      age: 20,
    },
  }

  await t.step('indent', async () => {
    const app = new cheetah()
      .use(pretty({
        indentSize: 4,
        sort: false,
      }))

    app.get('/', () => obj)

    const result = await app.fetch(new Request('http://localhost'))

    assertEquals(
      await result.text(),
      JSON.stringify(
        {
          lastname: 'Doe',
          firstname: 'John',
          data: {
            height: 180,
            age: 20,
          },
        },
        null,
        4,
      ),
    )

    assertEquals(
      result.headers.get('content-type'),
      'application/json; charset=utf-8',
    )
  })

  await t.step('sort', async () => {
    const app = new cheetah()
      .use(pretty())

    app.get('/', () => obj)

    const result = await app.fetch(new Request('http://localhost'))

    assertEquals(
      await result.text(),
      JSON.stringify(
        {
          data: {
            age: 20,
            height: 180,
          },
          firstname: 'John',
          lastname: 'Doe',
        },
        null,
        2,
      ),
    )

    assertEquals(
      result.headers.get('content-type'),
      'application/json; charset=utf-8',
    )
  })
})


================================================
FILE: test/extensions.test.ts
================================================
// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.
import { assertEquals } from 'std/assert/mod.ts'
import cheetah, { createExtension } from '../mod.ts'

Deno.test('Extensions', async (t) => {
  let count = 0

  const a = createExtension({
    onPlugIn({ setRoute }) {
      setRoute('get', '/cookie', () => '🍪')

      count++
    },
    onRequest({ req }) {
      req.headers.set('custom', 'test')
    },
  })

  const b = createExtension({
    onResponse({ c, prefix }) {
      c.res.body = 'custom'

      assertEquals(prefix, '/b')
    },
  })

  const c = createExtension({
    onRequest({ req }) {
      const url = new URL(req.url)

      if (url.pathname.startsWith('/foo')) {
        return new Response('hello')
      }
    },
  })

  const app = new cheetah()
    .use(a(), c())
    .use('/b', b())
    .get('/a', (c) => {
      assertEquals(c.req.headers.custom, 'test')

      return 'test'
    })
    .get('/b', (c) => {
      assertEquals(c.req.headers.custom, 'test')

      return 'test'
    })

  await t.step('onPlugIn (part 1)', async () => {
    const result = await app.fetch(new Request('http://localhost/cookie'))

    assertEquals(await result.text(), '🍪')
  })

  await t.step('onRequest', async () => {
    const result = await app.fetch(new Request('http://localhost/a'))

    assertEquals(await result.text(), 'test')
  })

  await t.step('onResponse', async () => {
    const result1 = await app.fetch(new Request('http://localhost/b'))

    assertEquals(await result1.text(), 'custom')

    const result2 = await app.fetch(new Request('http://localhost/foo/bar'))

    assertEquals(await result2.text(), 'hello')
  })

  await t.step('onPlugIn (part 2)', async () => {
    const result = await app.fetch(new Request('http://localhost/cookie'))

    assertEquals(await result.text(), '🍪')
    assertEquals(count, 1)
  })
})


================================================
FILE: test/jwt.test.ts
================================================
// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.
import { assertEquals } from 'std/assert/mod.ts'
import { cheetah } from '../cheetah.ts'
import { createJwtSecret } from '../cli/cmd/random/create_jwt_secret.ts'
import { jwt } from '../mod.ts'

Deno.test('jwt', async () => {
  const key = await createJwtSecret()

  const token = await jwt.sign(key, { example: 'object' })

  assertEquals(
    await jwt.verify(await createJwtSecret(), token) === undefined,
    true,
  )
  assertEquals(await jwt.verify(key, token) !== undefined, true)

  Deno.env.set('jwt_secret', key)

  const app = new cheetah()

  app.get('/one', async (c) => {
    assertEquals(await jwt.verify(c, token) !== undefined, true)
  })

  await app.fetch(new Request('http://localhost/one'))

  Deno.env.delete('jwt_secret')
  Deno.env.set('JWT_SECRET', key)

  app.get('/two', async (c) => {
    assertEquals(await jwt.verify(c, token) !== undefined, true)
  })

  await app.fetch(new Request('http://localhost/two'))
})


================================================
FILE: test/many_handlers.test.ts
================================================
// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.
import { assertEquals } from 'std/assert/mod.ts'
import cheetah from '../mod.ts'

Deno.test('Many Handlers', async () => {
  const app = new cheetah()

  app.get('/a', (_, next) => {
    next()

    return 'a'
  }, (c) => {
    if (c.req.headers.foo !== undefined) {
      return 'b'
    }
  })

  app.get('/b', (c) => {
    c.res.header('foo', 'bar')
  }, (c) => {
    c.res.header('foo', 'foo')
  })

  app.get('/c', (c, next) => {
    c.res.header('foo', 'bar')

    next()
  }, (c) => {
    c.res.header('foo', 'foo')
  })

  assertEquals(
    await (await app.fetch(new Request('http://localhost/a'))).text(),
    'a',
  )

  assertEquals(
    await (await app.fetch(
      new Request('http://localhost/a', { headers: { 'foo': 'bar' } }),
    )).text(),
    'b',
  )

  assertEquals(
    (await app.fetch(new Request('http://localhost/b'))).headers.get('foo'),
    'bar',
  )

  assertEquals(
    (await app.fetch(new Request('http://localhost/c'))).headers.get('foo'),
    'foo',
  )
})


================================================
FILE: test/preflight_mode.test.ts
================================================
// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.
import { assertEquals } from 'std/assert/mod.ts'
import cheetah from '../mod.ts'

Deno.test('preflight mode', async (t) => {
  await t.step('basic', async () => {
    const app = new cheetah({ preflight: true })

    app.get('/foo1', () => {
      return 'bar1'
    })

    app.post('/foo2', () => {
      return 'bar2'
    })

    const res1 = await app.fetch(new Request('https://deno.com/foo1'))

    assertEquals(res1.body === null, false)

    const res2 = await app.fetch(
      new Request('https://deno.com/foo1', { method: 'HEAD' }),
    )

    assertEquals(res2.body === null, true)

    const res3 = await app.fetch(
      new Request('https://deno.com/foo2', { method: 'POST' }),
    )

    assertEquals(res3.body === null, false)

    const res4 = await app.fetch(
      new Request('https://deno.com/foo2', { method: 'HEAD' }),
    )

    assertEquals(res4.status, 404)
    assertEquals(res4.body === null, true)
  })

  await t.step('not found', async () => {
    const app = new cheetah({ preflight: true })

    const res1 = await app.fetch(new Request('https://deno.com/foo'))

    assertEquals(res1.body === null, false)

    const res2 = await app.fetch(
      new Request('https://deno.com/foo', { method: 'HEAD' }),
    )

    assertEquals(res2.body === null, true)
  })

  await t.step('not found (custom)', async () => {
    const app = new cheetah({
      preflight: true,
      notFound() {
        return new Response('custom', {
          headers: {
            foo: 'bar',
          },
        })
      },
    })

    const res1 = await app.fetch(new Request('https://deno.com'))

    assertEquals(res1.body === null, false)
    assertEquals(res1.headers.get('foo'), 'bar')

    const res2 = await app.fetch(
      new Request('https://deno.com', { method: 'HEAD' }),
    )

    assertEquals(res2.body === null, true)
    assertEquals(res2.headers.get('foo'), 'bar')
  })

  await t.step('error', async () => {
    const app = new cheetah({ preflight: true })

    app.get('/foo', () => {
      throw new Error()
    })

    const res1 = await app.fetch(new Request('https://deno.com/foo'))

    assertEquals(res1.body === null, false)

    const res2 = await app.fetch(
      new Request('https://deno.com/foo', { method: 'HEAD' }),
    )

    assertEquals(res2.body === null, true)
  })

  await t.step('error (custom)', async () => {
    const app = new cheetah({
      preflight: true,
      error() {
        return new Response('custom', {
          headers: {
            foo: 'bar',
          },
        })
      },
    })

    app.get('/foo', () => {
      throw new Error()
    })

    const res1 = await app.fetch(new Request('https://deno.com/foo'))

    assertEquals(res1.body === null, false)
    assertEquals(res1.headers.get('foo'), 'bar')

    const res2 = await app.fetch(
      new Request('https://deno.com/foo', { method: 'HEAD' }),
    )

    assertEquals(res2.body === null, true)
    assertEquals(res2.headers.get('foo'), 'bar')
  })
})


================================================
FILE: test/render.test.tsx
================================================
// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.
/** @jsx h */
import { DOMParser } from 'dom'
import { Fragment, h, VNode } from 'preact'
import { assert, assertEquals } from 'std/assert/mod.ts'
import { z } from 'zod'
import cheetah, { Renderer } from '../mod.ts'

const Document = ({ children }: { children: VNode }) => {
  return (
    <html>
      <head>
        <title>This is a document!</title>
      </head>
      <body>
        {children}
      </body>
    </html>
  )
}

const Styled = () => {
  return (
    <Document>
      <h3 class='text-sm italic' id='styled'>
        styled <code class='font-mono'>h3</code> component
      </h3>
    </Document>
  )
}

const Unstyled = () => {
  return (
    <Document>
      <h3 id='unstyled'>
        unstyled <code>h3</code> component
      </h3>
    </Document>
  )
}

const MetaTagsWithoutWrappers = () => {
  return (
    <Fragment>
      <title>This is a document!</title>
      <meta charSet='utf-8' />
      <h1>Hello world!</h1>
    </Fragment>
  )
}

const app = new cheetah()

const { render } = new Renderer()

app.get('/render', {
  query: z.object({
    type: z.union([
      z.literal('styled'),
      z.literal('unstyled'),
      z.literal('meta-tags-without-wrappers'),
    ]),
  }),
}, (c) => {
  const type = c.req.query.type

  if (type === 'meta-tags-without-wrappers') {
    return render(c, <MetaTagsWithoutWrappers />)
  }

  render(
    c,
    type === 'styled' ? <Styled /> : <Unstyled />,
  )
})

Deno.test('render', async (test) => {
  await test.step('Twind styles are applied to the resulting HTML correctly.', async () => {
    const renderResponse = await app.fetch(
      new Request('http://localhost/render?type=styled'),
    )

    const htmlText = await renderResponse.text()

    const document = new DOMParser().parseFromString(
      htmlText,
      'text/html',
    )

    assert(document)
    assert([...document.getElementsByTagName('style')].length === 1)
    assertEquals(
      document.getElementById('styled')?.innerText,
      'styled h3 component',
    )
    assertEquals(
      renderResponse.headers.get('content-type'),
      'text/html; charset=utf-8',
    )
  })

  const renderResponse = await app.fetch(
    new Request('http://localhost/render?type=unstyled'),
  )

  const htmlText = await renderResponse.text()

  const document = new DOMParser().parseFromString(
    htmlText,
    'text/html',
  )

  assert(document)

  await test.step('No empty style tag is injected if no Twind styles are utilised.', () => {
    assertEquals(
      [...document.getElementsByTagName('style')].length,
      0,
    )
    assertEquals(
      document.getElementById('unstyled')?.innerText,
      'unstyled h3 component',
    )
    assertEquals(
      renderResponse.headers.get('content-type'),
      'text/html; charset=utf-8',
    )
  })

  await test.step('Head meta tags are able to be injected into the HTML output properly.', () => {
    const headElementsInDocument = [...document.getElementsByTagName('head')]

    assert(headElementsInDocument.length === 1)

    const headElementInDocument = headElementsInDocument.at(0)

    assert(headElementInDocument)
    assert(
      [...headElementInDocument.children].find((childNode) =>
        childNode.tagName === 'TITLE' &&
        childNode.textContent === 'This is a document!'
      ),
    )
  })

  await test.step('Even without the essential html, head and body tags in the JSX input, the JSX input is injected correctly into the appropriate places.', async () => {
    const renderResponse = await app.fetch(
      new Request('http://localhost/render?type=meta-tags-without-wrappers'),
    )

    const htmlText = await renderResponse.text()

    const document = new DOMParser().parseFromString(
      htmlText,
      'text/html',
    )
    assert(document)

    const { documentElement } = document
    assert(documentElement)

    const headElementsInDocument = documentElement.getElementsByTagName('head')
    const [headElement] = headElementsInDocument
    assert(headElement)

    const titleElementInDocument = headElement.getElementsByTagName('title').at(
      0,
    )
    assert(
      titleElementInDocument &&
        titleElementInDocument.textContent === 'This is a document!',
    )

    const metaTagsInDocument = documentElement.getElementsByTagName('meta')
    assert(
      metaTagsInDocument.find((metaTag) =>
        metaTag.getAttribute('charset') === 'utf-8'
      ),
    )

    const [bodyElement] = documentElement.getElementsByTagName('body')
    assert(bodyElement)

    const [headingElementInBody] = [...bodyElement.children]
    assert(
      headingElementInBody !== undefined &&
        headingElementInBody.tagName === 'H1' &&
        headingElementInBody.textContent === 'Hello world!',
    )
  })
})


================================================
FILE: test/request.test.ts
================================================
// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.
import { assertEquals } from 'std/assert/mod.ts'
import { z } from 'zod'
import cheetah from '../mod.ts'

Deno.test('Request', async (t) => {
  await t.step('req.param()', async () => {
    const app = new cheetah()

    app.get('/users/:name', (c) => c.req.param('name'))
    assertEquals(
      await (await app.fetch(
        new Request('http://localhost/users/johndoe'),
      )).text(),
      'johndoe',
    )
  })

  // await t.step('req.ip', async () => {
  //   const app = new cheetah()

  //   app.get('/ip', (c) => {
  //     return c.req.ip
  //   })

  //   const result = await (await app.fetch(new Request('http://localhost/ip')))
  //     .text()
  //   assertEquals(result, '127.0.0.1')
  // })

  await t.step('req.raw()', async () => {
    const app = new cheetah()

    app.post('/raw', (c) => c.req.raw.method)

    assertEquals(
      await (await app.fetch(
        new Request('http://localhost/raw', { method: 'POST' }),
      )).text(),
      'POST',
    )
  })

  await t.step('req.body()', async (t) => {
    const app = new cheetah()

    const text = async (pathname: string, body: BodyInit) => {
      try {
        return await (await app.fetch(
          new Request(`http://localhost/${pathname}`, {
            method: 'POST',
            body,
          }),
        )).text()
      } catch (_err) {
        return null
      }
    }

    await t.step('req.body() - union (object, object)', async () => {
      app.post('/b', {
        body: z.union([
          z.object({
            bar: z.literal('foo'),
          }),
          z.object({
            foo: z.literal('bar'),
          }),
        ]),
      }, async (c) => {
        await c.req.body()

        return 'ok'
      })

      assertEquals(
        await text(
          'b',
          'test',
        ),
        'Bad Request',
      )

      assertEquals(
        await text(
          'b',
          JSON.stringify({
            foo: 'bar',
          }),
        ),
        'ok',
      )

      assertEquals(
        await text(
          'b',
          JSON.stringify({
            bar: 'foo',
          }),
        ),
        'ok',
      )

      assertEquals(
        await text(
          'b',
          JSON.stringify({
            abc: 'def',
          }),
        ),
        'Bad Request',
      )
    })

    await t.step('req.body() - union (string, string)', async () => {
      app.post('/c', {
        body: z.union([
          z.string().startsWith('http://'),
          z.string().startsWith('https://'),
        ]),
      }, async (c) => {
        await c.req.body()

        return 'ok'
      })

      assertEquals(
        await text('c', 'foo'),
        'Bad Request',
      )

      assertEquals(
        await text(
          'c',
          JSON.stringify({
            foo: 'bar',
          }),
        ),
        'Bad Request',
      )

      assertEquals(
        await text('c', 'https://foo.bar'),
        'ok',
      )

      assertEquals(
        await text('c', 'http://foo.bar'),
        'ok',
      )
    })

    await t.step('req.body() - string', async () => {
      app.post('/d', {
        body: z.string().max(3),
      }, async (c) => {
        await c.req.body()

        return 'ok'
      })

      assertEquals(
        await text('d', 'foo'),
        'ok',
      )

      assertEquals(
        await text('d', 'bar'),
        'ok',
      )

      assertEquals(
        await text(
          'd',
          'foo bar',
        ),
        'Bad Request',
      )
    })

    await t.step('req.body() - object', async () => {
      app.post('/e', {
        body: z.object({
          foo: z.literal('bar'),
        }),
      }, async (c) => {
        await c.req.body()

        return 'ok'
      })

      assertEquals(
        await text('e', 'test'),
        'Bad Request',
      )

      assertEquals(
        await text(
          'e',
          JSON.stringify({
            foo: 'bar',
          }),
        ),
        'ok',
      )

      assertEquals(
        await text(
          'e',
          JSON.stringify({
            abc: 'def',
          }),
        ),
        'Bad Request',
      )
    })

    await t.step('req.body() - record', async () => {
      app.post('/f', {
        body: z.record(z.string()),
      }, async (c) => {
        await c.req.body()

        return 'ok'
      })

      assertEquals(
        await text('f', 'test'),
        'Bad Request',
      )

      assertEquals(
        await text(
          'f',
          JSON.stringify({
            foo: 'bar',
          }),
        ),
        'ok',
      )

      assertEquals(
        await text(
          'f',
          JSON.stringify({
            abc: 'def',
          }),
        ),
        'ok',
      )
    })
  })

  await t.step('req.buffer()', async () => {
    const app = new cheetah()

    app.post(
      '/buffer',
      async (c) => await c.req.buffer() instanceof ArrayBuffer ? 'ok' : 'error',
    )

    assertEquals(
      await (await app.fetch(
        new Request('http://localhost/buffer', {
          method: 'POST',
          body: 'test',
        }),
      )).text(),
      'ok',
    )
  })

  await t.step('req.blob()', async () => {
    const app = new cheetah()

    app.post(
      '/blob',
      async (c) => await c.req.blob() instanceof Blob ? 'ok' : 'error',
    )

    assertEquals(
      await (await app.fetch(
        new Request('http://localhost/blob', {
          method: 'POST',
          body: 'test',
        }),
      )).text(),
      'ok',
    )
  })

  await t.step('req.formData()', async () => {
    const app = new cheetah()

    app.post(
      '/formData',
      async (c) => await c.req.formData() instanceof FormData ? 'ok' : 'error',
    )
    app.post(
      '/notFormData',
      async (c) => await c.req.formData() === null ? 'ok' : 'error',
    )

    assertEquals(
      await (await app.fetch(
        new Request('http://localhost/formData', {
          method: 'POST',
          body: new FormData(),
        }),
      )).text(),
      'ok',
    )
    assertEquals(
      await (await app.fetch(
        new Request('http://localhost/notFormData', {
          method: 'POST',
          body: 'test',
        }),
      )).text(),
      'ok',
    )
  })

  await t.step('req.stream()', async () => {
    const app = new cheetah()

    app.post('/stream', async (c) => {
      const stream = c.req.stream

      const text = stream !== null && stream instanceof ReadableStream
        ? 'ok'
        : 'error'

      if (stream !== null) {
        await stream.cancel()
      }

      return text
    })

    assertEquals(
      await (await app.fetch(
        new Request('http://localhost/stream', {
          method: 'POST',
          body: new ReadableStream(),
        }),
      )).text(),
      'ok',
    )
  })
})


================================================
FILE: test/response.test.ts
================================================
// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.
import { assertEquals } from 'std/assert/mod.ts'
import cheetah from '../mod.ts'

Deno.test('Response', async (t) => {
  const app = new cheetah()

  await t.step('res.code', async () => {
    app.get('/code-explicit', (c) => {
      c.res.code = 101
    })
    assertEquals(
      (await app.fetch(new Request('http://localhost/code-explicit'))).status,
      101,
    )

    app.get('/code-implicit', () => {
      return {
        code: 403,
      }
    })
    assertEquals(
      (await app.fetch(new Request('http://localhost/code-implicit'))).status,
      403,
    )
  })

  await t.step('res.bodySize', async () => {
    app.get('/body_size', (c) => {
      c.res.body = 'hello world'

      return c.res.bodySize.toString()
    })

    const result = await app.fetch(
      new Request('http://localhost/body_size'),
    )

    assertEquals(
      await result.text(),
      'hello world'.length.toString(),
    )
  })

  await t.step('res.setCookie()', async () => {
    app.get('/cookie', (c) => c.res.setCookie('custom', 'test'))
    assertEquals(
      (await app.fetch(new Request('http://localhost/cookie'))).headers
        .get('set-cookie'),
      'custom=test;',
    )
  })

  // TODO add test suite for res.deleteCookie()

  await t.step('res.header()', async () => {
    app.get('/header', (c) => c.res.header('custom', 'test'))
    assertEquals(
      (await app.fetch(new Request('http://localhost/header'))).headers
        .get('custom'),
      'test',
    )
  })

  await t.step('res.redirect()', async () => {
    // temporary redirect
    app.get('/temporaryredirect', (c) => c.res.redirect('https://deno.com'))

    const response1 = await app.fetch(
      new Request('http://localhost/temporaryredirect'),
    )
    assertEquals(response1.status, 307)
    assertEquals(response1.headers.get('location'), 'https://deno.com')

    // permanent redirect
    app.get(
      '/permanentredirect',
      (c) => c.res.redirect('https://deno.com', 301),
    )

    const response2 = await app.fetch(
      new Request('http://localhost/permanentredirect'),
    )
    assertEquals(response2.status, 301)
    assertEquals(response2.headers.get('location'), 'https://deno.com')
  })

  await t.step('blob', async () => {
    const blob = new Blob([new TextEncoder().encode('test')])

    app.get('/blob', () => {
      return blob
    })

    assertEquals(
      new TextDecoder().decode(
        await (await (await app.fetch(
          new Request('http://localhost/blob'),
        )).blob()).arrayBuffer(),
      ),
      'test',
    )
  })

  await t.step('stream', async () => {
    app.get('/stream', () => {
      return new ReadableStream()
    })

    assertEquals(
      (await app.fetch(new Request('http://localhost/stream'))).body !==
        null,
      true,
    )
  })

  await t.step('formData', async () => {
    const formData = new FormData()
    formData.append('one', 'field')

    app.get('/formData', () => {
      return formData
    })

    assertEquals(
      await (await app.fetch(new Request('http://localhost/formData')))
        .formData(),
      formData,
    )
  })

  await t.step('buffer', async () => {
    const buffer = new TextEncoder().encode('test')
    const arrayBuffer = buffer.buffer

    app.get('/buffer', () => {
      return buffer
    })

    assertEquals(
      await (await app.fetch(new Request('http://localhost/buffer')))
        .arrayBuffer(),
      arrayBuffer,
    )
  })

  await t.step('json', async () => {
    app.get('/json', () => {
      return {
        message: 'test',
      }
    })

    assertEquals(
      await (await app.fetch(
        new Request('http://localhost/json'),
      )).json(),
      { message: 'test' },
    )
  })

  await t.step('text', async () => {
    app.get('/text', () => {
      return 'test'
    })

    assertEquals(
      await (await app.fetch(
        new Request('http://localhost/text'),
      )).text(),
      'test',
    )
  })
})


================================================
FILE: test/routing/versioning.test.ts
================================================
import { assertEquals } from 'std/assert/mod.ts'
import { cheetah } from '../../cheetah.ts'

Deno.test('versioning', async (t) => {
  /* uri ---------------------------------------------------------------------- */

  await t.step('uri', async () => {
    const app = new cheetah({
      versioning: {
        current: 'v4', // latest
        type: 'uri',
      },
    })

    async function get(path: string) {
      const res = await app.fetch(new Request(`http://localhost${path}`))

      return await res.text()
    }

    // unspecified

    app.get('/1', (c) => `hello from v${c.req.gateway}`)

    assertEquals(await get('/v1/1'), 'hello from v1')
    assertEquals(await get('/v2/1'), 'hello from v2')
    assertEquals(await get('/v3/1'), 'hello from v3')
    assertEquals(await get('/v4/1'), 'hello from v4')
    assertEquals(await get('/v5/1') !== 'hello from v5', true)

    // exact

    app.get('/2', { gateway: 'v3' }, (c) => `hello from v${c.req.gateway}`)

    assertEquals(await get('/v1/2') !== 'hello from v1', true)
    assertEquals(await get('/v2/2') !== 'hello from v2', true)
    assertEquals(await get('/v3/2'), 'hello from v3')
    assertEquals(await get('/v4/2') !== 'hello from v4', true)

    // smaller than

    app.get('/3', { gateway: '< v3' }, (c) => `hello from v${c.req.gateway}`)

    assertEquals(await get('/v1/3'), 'hello from v1')
    assertEquals(await get('/v2/3'), 'hello from v2')
    assertEquals(await get('/v3/3') !== 'hello from v3', true)
    assertEquals(await get('/v4/3') !== 'hello from v4', true)

    // smaller than or equal to

    app.get('/4', { gateway: '<= v3' }, (c) => `hello from v${c.req.gateway}`)

    assertEquals(await get('/v1/4'), 'hello from v1')
    assertEquals(await get('/v2/4'), 'hello from v2')
    assertEquals(await get('/v3/4'), 'hello from v3')
    assertEquals(await get('/v4/4') !== 'hello from v4', true)

    // greater than

    app.get('/5', { gateway: '> v3' }, (c) => `hello from v${c.req.gateway}`)

    assertEquals(await get('/v1/5') !== 'hello from v1', true)
    assertEquals(await get('/v2/5') !== 'hello from v2', true)
    assertEquals(await get('/v3/5') !== 'hello from v3', true)
    assertEquals(await get('/v4/5'), 'hello from v4')

    // greater than or equal

    app.get('/6', { gateway: '>= v3' }, (c) => `hello from v${c.req.gateway}`)

    assertEquals(await get('/v1/6') !== 'hello from v1', true)
    assertEquals(await get('/v2/6') !== 'hello from v2', true)
    assertEquals(await get('/v3/6'), 'hello from v3')
    assertEquals(await get('/v4/6'), 'hello from v4')

    // from (min) ... to (max)

    app.get(
      '/7',
      { gateway: 'v2...v3' },
      (c) => `hello from v${c.req.gateway}`,
    )

    assertEquals(await get('/v1/7') !== 'hello from v1', true)
    assertEquals(await get('/v2/7'), 'hello from v2')
    assertEquals(await get('/v3/7'), 'hello from v3')
    assertEquals(await get('/v4/7') !== 'hello from v4', true)

    // with base path
    const app2 = new cheetah({
      base: '/api',
      versioning: {
        current: 'v4', // latest
        type: 'uri',
      },
    })

    async function get2(path: string) {
      const res = await app2.fetch(new Request(`http://localhost${path}`))

      return await res.text()
    }

    app2.get(
      '/7',
      { gateway: 'v2...v3' },
      (c) => `hello from v${c.req.gateway}`,
    )

    assertEquals(await get2('/api/v1/7') !== 'hello from v1', true)
    assertEquals(await get2('/api/v2/7'), 'hello from v2')
    assertEquals(await get2('/api/v3/7'), 'hello from v3')
    assertEquals(await get2('/api/v4/7') !== 'hello from v4', true)
  })

  /* header ------------------------------------------------------------------- */

  await t.step('header', async () => {
    const app = new cheetah({
      versioning: {
        current: 'v4', // latest
        type: 'header',
        header: 'x-version',
      },
    })

    async function get(path: string) {
      path = path.replace('/', '')

      const res = await app.fetch(
        new Request(`http://localhost/${path.split('/')[1]}`, {
          headers: {
            'x-version': `${path.split('/')[0]}`,
          },
        }),
      )

      return await res.text()
    }

    // unspecified

    app.get('/1', (c) => `hello from v${c.req.gateway}`)

    assertEquals(await get('/v1/1'), 'hello from v1')
    assertEquals(await get('/v2/1'), 'hello from v2')
    assertEquals(await get('/v3/1'), 'hello from v3')
    assertEquals(await get('/v4/1'), 'hello from v4')
    assertEquals(await get('/v5/1') !== 'hello from v5', true)

    // exact

    app.get('/2', { gateway: 'v3' }, (c) => `hello from v${c.req.gateway}`)

    assertEquals(await get('/v1/2') !== 'hello from v1', true)
    assertEquals(await get('/v2/2') !== 'hello from v2', true)
    assertEquals(await get('/v3/2'), 'hello from v3')
    assertEquals(await get('/v4/2') !== 'hello from v4', true)

    // smaller than

    app.get('/3', { gateway: '< v3' }, (c) => `hello from v${c.req.gateway}`)

    assertEquals(await get('/v1/3'), 'hello from v1')
    assertEquals(await get('/v2/3'), 'hello from v2')
    assertEquals(await get('/v3/3') !== 'hello from v3', true)
    assertEquals(await get('/v4/3') !== 'hello from v4', true)

    // smaller than or equal to

    app.get('/4', { gateway: '<= v3' }, (c) => `hello from v${c.req.gateway}`)

    assertEquals(await get('/v1/4'), 'hello from v1')
    assertEquals(await get('/v2/4'), 'hello from v2')
    assertEquals(await get('/v3/4'), 'hello from v3')
    assertEquals(await get('/v4/4') !== 'hello from v4', true)

    // greater than

    app.get('/5', { gateway: '> v3' }, (c) => `hello from v${c.req.gateway}`)

    assertEquals(await get('/v1/5') !== 'hello from v1', true)
    assertEquals(await get('/v2/5') !== 'hello from v2', true)
    assertEquals(await get('/v3/5') !== 'hello from v3', true)
    assertEquals(await get('/v4/5'), 'hello from v4')

    // greater than or equal

    app.get('/6', { gateway: '>= v3' }, (c) => `hello from v${c.req.gateway}`)

    assertEquals(await get('/v1/6') !== 'hello from v1', true)
    assertEquals(await get('/v2/6') !== 'hello from v2', true)
    assertEquals(await get('/v3/6'), 'hello from v3')
    assertEquals(await get('/v4/6'), 'hello from v4')

    // from (min) ... to (max)

    app.get(
      '/7',
      { gateway: 'v2...v3' },
      (c) => `hello from v${c.req.gateway}`,
    )

    assertEquals(await get('/v1/7') !== 'hello from v1', true)
    assertEquals(await get('/v2/7'), 'hello from v2')
    assertEquals(await get('/v3/7'), 'hello from v3')
    assertEquals(await get('/v4/7') !== 'hello from v4', true)
  })
})


================================================
FILE: test/routing.test.ts
================================================
// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.
import { assertEquals } from 'std/assert/mod.ts'
import cheetah, { Collection } from '../mod.ts'

Deno.test('Nesting/Routing', async (t) => {
  const collection = new Collection()
    .get('/', () => 'nested root route')
    .get('/nestedroute', () => 'nested route')

  await t.step('Root with Base Path', async () => {
    const r1 = new cheetah({
      base: '/api',
    })
      .use('/collection', collection)

    assertEquals(
      await (await r1.fetch(
        new Request('http://localhost/api/collection'),
      )).text(),
      'nested root route',
    )
    assertEquals(
      await (await r1.fetch(
        new Request('http://localhost/api/collection/nestedroute'),
      )).text(),
      'nested route',
    )
  })

  await t.step('Root without Base Path', async () => {
    const r2 = new cheetah()
      .use('/collection', collection)

    assertEquals(
      await (await r2.fetch(new Request('http://localhost/collection')))
        .text(),
      'nested root route',
    )
    assertEquals(
      await (await r2.fetch(
        new Request('http://localhost/collection/nestedroute'),
      )).text(),
      'nested route',
    )
  })
})


================================================
FILE: test/validation.test.ts
================================================
// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.
import { assertEquals } from 'std/assert/mod.ts'
import { z } from 'zod'
import cheetah from '../mod.ts'

Deno.test('Validation', async (t) => {
  await t.step('transform', async () => {
    const app = new cheetah()

    app.post('/transform', {
      body: z.object({
        message: z.string(),
      }),
    }, async (c) => {
      assertEquals(await c.req.body({ transform: true }), {
        message: 'Hello World',
      })

      return 'test'
    })

    const form = new FormData()
    form.append('message', 'Hello World')
    await (await app.fetch(
      new Request('http://localhost/transform', {
        method: 'POST',
        body: form,
      }),
    )).text()

    await (await app.fetch(
      new Request('http://localhost/transform', {
        method: 'POST',
        body: JSON.stringify({ message: 'Hello World' }),
      }),
    )).text()
  })

  await t.step('cookies', async () => {
    const app = new cheetah()

    app.get('/cookies', {
      cookies: z.object({ custom: z.string().min(4).max(16) }).strict(),
    }, (c) => {
      return c.req.cookies
    })

    assertEquals(
      (await app.fetch(
        new Request('http://localhost/cookies', {
          headers: { cookies: 'custom=test;' },
        }),
      )).status,
      200,
    )
    assertEquals(
      (await app.fetch(
        new Request('http://localhost/cookies', {
          headers: { cookie: 'custom=te;' },
        }),
      )).status,
      400,
    )
    assertEquals(
      (await app.fetch(
        new Request('http://localhost/cookies', {
          headers: { cookie: 'invalid=abc;' },
        }),
      )).status,
      400,
    )
    assertEquals(
      (await app.fetch(
        new Request('http://localhost/cookies', {
          headers: { cookie: 'custom=test; another=cookie;' },
        }),
      )).status,
      400,
    )
  })

  await t.step('headers', async () => {
    const app = new cheetah()

    app.get(
      '/headers',
      { headers: z.object({ custom: z.string().email() }) },
      (c) => {
        return c.req.headers
      },
    )

    const res1 = await app.fetch(
      new Request('http://localhost/headers', {
        headers: { random: 'bullshit' },
      }),
    )
    const res2 = await app.fetch(
      new Request('http://localhost/headers', {
        headers: { random: 'bullshit', custom: 'tes@t' },
      }),
    )
    const res3 = await app.fetch(
      new Request('http://localhost/headers', {
        headers: { custom: '' },
      }),
    )
    const res4 = await app.fetch(
      new Request('http://localhost/headers', {
        headers: { custom: 'test@email.com' },
      }),
    )

    assertEquals(res1.status, 400)
    assertEquals(res2.status, 400)
    assertEquals(res3.status, 400)
    assertEquals(res4.status, 200)
    assertEquals(await res4.json(), { custom: 'test@email.com' })
  })

  await t.step('query', async () => {
    const app = new cheetah()

    app.get('/query', {
      query: z.object({
        first: z.string().optional(),
        second: z.boolean(),
        third: z.number(),
      }),
    }, (c) => {
      const d = c.req.query

      d.first

      return c.req.query
    })

    assertEquals(
      (await app.fetch(
        new Request('http://localhost/query?first=test&second&third=69'),
      )).status,
      200,
    )
    assertEquals(
      (await app.fetch(
        new Request('http://localhost/query?second=false&third=e9'),
      )).status,
      400,
    )
    assertEquals(
      (await app.fetch(
        new Request('http://localhost/query?second=false&third=69'),
      )).status,
      200,
    )
  })

  await t.step('query (no schema)', async () => {
    const app = new cheetah()

    app.get('/query', (c) => {
      const d = c.req.query

      d.lol
      return d
    })

    assertEquals(
      await (await app.fetch(
        new Request('http://localhost/query?first=test&second&third=69'),
      )).json(),
      { first: 'test', second: true, third: 69 },
    )
    assertEquals(
      await (await app.fetch(
        new Request('http://localhost/query?second=false&third=e9'),
      )).json(),
      { second: false, third: 'e9' },
    )
    assertEquals(
      await (await app.fetch(
        new Request('http://localhost/query?second=false&third=69'),
      )).json(),
      { second: false, third: 69 },
    )
  })

  await t.step('params', async () => {
    const app = new cheetah()

    app.get(
      '/animals/:name',
      {
        params: {
          name: z.union([z.literal('cat'), z.literal('dog')]),
        },
      },
      (c) => {
        return c.req.param('name')
      },
    )

    const res1 = await app.fetch(
      new Request('http://localhost/animals/cat'),
    )

    assertEquals(res1.status, 200)

    const res2 = await app.fetch(
      new Request('http://localhost/animals/dog'),
    )

    assertEquals(res2.status, 200)

    const res3 = await app.fetch(
      new Request('http://localhost/animals/rabbit'),
    )

    assertEquals(res3.status, 400)
  })
})
Download .txt
gitextract_960kgzwh/

├── .gitattributes
├── .github/
│   ├── dependabot.yml
│   ├── funding.yml
│   └── workflows/
│       ├── check.yml
│       ├── publish.yml
│       └── update.yml
├── .vscode/
│   ├── extensions.json
│   └── settings.json
├── base.ts
├── cheetah.ts
├── cli/
│   ├── cmd/
│   │   ├── bundle/
│   │   │   ├── bundle.ts
│   │   │   └── mod.ts
│   │   ├── new/
│   │   │   └── mod.ts
│   │   ├── random/
│   │   │   ├── create_crypto_key.ts
│   │   │   ├── create_jwt_secret.ts
│   │   │   └── mod.ts
│   │   └── serve/
│   │       └── mod.ts
│   ├── mod.ts
│   └── utils.ts
├── codeowners
├── collection.ts
├── context.ts
├── contributing.md
├── crypto.ts
├── deno.json
├── env.d.ts
├── ext/
│   ├── compress.ts
│   ├── debug.ts
│   ├── favicon.ts
│   ├── files.ts
│   ├── firewall.ts
│   ├── helmet.ts
│   └── pretty.ts
├── extensions.ts
├── handler.ts
├── jwt.ts
├── license
├── location_data.ts
├── mod.ts
├── oauth/
│   ├── client.ts
│   ├── get_session_data.ts
│   ├── get_session_id.ts
│   ├── get_session_token.ts
│   ├── handle_callback.ts
│   ├── is_signed_in.ts
│   ├── mod.ts
│   ├── sign_in.ts
│   ├── sign_out.ts
│   ├── store.ts
│   └── types.ts
├── otp.ts
├── readme.md
├── render.ts
├── request_context.ts
├── response_context.ts
├── send_mail.ts
└── test/
    ├── context.test.ts
    ├── cors.test.ts
    ├── exception.test.ts
    ├── ext/
    │   ├── favicon.test.ts
    │   ├── files.test.ts
    │   ├── firewall.test.ts
    │   └── pretty.test.ts
    ├── extensions.test.ts
    ├── jwt.test.ts
    ├── many_handlers.test.ts
    ├── preflight_mode.test.ts
    ├── render.test.tsx
    ├── request.test.ts
    ├── response.test.ts
    ├── routing/
    │   └── versioning.test.ts
    ├── routing.test.ts
    └── validation.test.ts
Download .txt
SYMBOL INDEX (187 symbols across 43 files)

FILE: base.ts
  type Method (line 4) | type Method =
  constant METHODS (line 12) | const METHODS: Method[] = [
  function base (line 21) | function base<T>(): {

FILE: cheetah.ts
  type AppContext (line 17) | type AppContext = {
  type AppConfig (line 36) | type AppConfig = {
  class cheetah (line 114) | class cheetah extends base<cheetah>() {
    method constructor (line 130) | constructor({
    method use (line 196) | use<C extends Collection>(
    method routes (line 248) | get routes() {
    method #parseVersion (line 254) | #parseVersion(headers: Headers, pathname: string) {
    method #match (line 297) | #match(request: Request, p: string) {
    method #handle (line 546) | async #handle(
    method serve (line 715) | serve({
  function isVersionWithinRange (line 731) | function isVersionWithinRange(

FILE: cli/cmd/bundle/bundle.ts
  function bundle (line 6) | async function bundle({

FILE: cli/cmd/bundle/mod.ts
  function bundleCommand (line 13) | async function bundleCommand(args: ReturnType<typeof parse>) {

FILE: cli/cmd/new/mod.ts
  type VSCodeSettings (line 5) | type VSCodeSettings = {
  function newCommand (line 11) | async function newCommand() {

FILE: cli/cmd/random/create_crypto_key.ts
  function createCryptoKey (line 4) | async function createCryptoKey() {

FILE: cli/cmd/random/create_jwt_secret.ts
  function createJwtSecret (line 4) | async function createJwtSecret() {

FILE: cli/cmd/random/mod.ts
  function randomCommand (line 7) | async function randomCommand() {

FILE: cli/cmd/serve/mod.ts
  function serveCommand (line 9) | async function serveCommand(args: ReturnType<typeof parse>) {

FILE: cli/utils.ts
  function logError (line 4) | function logError(message: string) {

FILE: collection.ts
  class Collection (line 5) | class Collection extends base<Collection>() {
    method constructor (line 16) | constructor({

FILE: context.ts
  constant HTTP_MESSAGES (line 10) | const HTTP_MESSAGES = {
  class Context (line 36) | class Context<
    method constructor (line 64) | constructor(
    method __app (line 90) | get __app(): AppContext {
    method cache (line 94) | get cache(): Cache {
    method dev (line 104) | get dev(): boolean {
    method env (line 109) | env<T extends keyof Variables>(name: T): Variables[T] {
    method req (line 115) | get req(): RequestContext<
    method res (line 137) | get res(): ResponseContext {
    method runtime (line 147) | get runtime() {
    method exception (line 151) | exception(error: keyof typeof HTTP_MESSAGES, description?: string) {
  class Exception (line 159) | class Exception {
    method constructor (line 162) | constructor(
  class Cache (line 190) | class Cache {
    method constructor (line 195) | constructor(c: Context) {
    method set (line 201) | async set(
    method get (line 242) | async get<T extends Record<string, unknown> | string | Uint8Array>(
    method has (line 271) | async has(key: string) {
    method delete (line 283) | async delete(key: string) {

FILE: crypto.ts
  function encrypt (line 5) | async function encrypt(c: Context, message: string) {
  function decrypt (line 32) | async function decrypt(c: Context, message: string) {

FILE: env.d.ts
  type Variables (line 2) | type Variables = Record<string, unknown>

FILE: ext/compress.ts
  type CompressionAlgorithm (line 6) | type CompressionAlgorithm = {
  constant FORAS (line 11) | let FORAS: InitOutput | undefined
  constant BROTLI (line 13) | const BROTLI: CompressionAlgorithm = {
  constant DEFLATE (line 18) | const DEFLATE: CompressionAlgorithm = {
  method compress (line 20) | async compress(input) {
  constant GZIP (line 29) | const GZIP: CompressionAlgorithm = {
  method compress (line 31) | async compress(input) {
  method onResponse (line 48) | async onResponse({ c, _ }) {

FILE: ext/debug.ts
  method onResponse (line 18) | onResponse({ c }) {

FILE: ext/favicon.ts
  constant FAVICON (line 4) | let FAVICON: ArrayBuffer | Uint8Array | undefined
  method onRequest (line 17) | async onRequest({

FILE: ext/files.ts
  type GeneralOptions (line 8) | type GeneralOptions = {
  type FsOptions (line 13) | type FsOptions = {
  type R2Options (line 18) | type R2Options = {
  type S3Options (line 23) | type S3Options = {
  function getVar (line 35) | function getVar<T extends unknown = string | undefined>(
  method onRequest (line 61) | onRequest({
  function handleS3Files (line 91) | async function handleS3Files(
  function handleR2Files (line 142) | async function handleR2Files(
  function handleFsFiles (line 190) | async function handleFsFiles(
  function etag (line 230) | async function etag(stat: Deno.FileInfo) {

FILE: ext/firewall.ts
  constant VPN_LIST (line 3) | let VPN_LIST: string[] = []
  constant DATACENTER_LIST (line 4) | let DATACENTER_LIST: string[] = []
  constant LAST_UPDATE (line 5) | let LAST_UPDATE = 0
  type FirewallOptions (line 7) | type FirewallOptions = {
  method onRequest (line 20) | async onRequest({ _: opts, app }) {
  function checkUpdate (line 38) | async function checkUpdate() {
  function isIpInRange (line 49) | function isIpInRange(ip: string, range: string) {
  function ipToNumber (line 57) | function ipToNumber(ip: string) {

FILE: ext/helmet.ts
  method onResponse (line 134) | onResponse({

FILE: ext/pretty.ts
  function sortObject (line 4) | function sortObject(object: unknown) {
  method onResponse (line 41) | onResponse({

FILE: extensions.ts
  type HasRequired (line 7) | type HasRequired<T> = Partial<T> extends T ? false : true
  type Req (line 9) | type Req = Response | void | undefined
  type Res (line 10) | type Res = void | undefined
  type ExtensionContext (line 12) | type ExtensionContext = {
  type Extension (line 17) | type Extension<
  type ReturnFunction (line 121) | type ReturnFunction<
  function validExtension (line 128) | function validExtension(ext: Record<string, unknown>) {
  function createExtension (line 137) | function createExtension<

FILE: handler.ts
  type ObjectType (line 12) | type ObjectType =
  type BaseType (line 17) | type BaseType<T extends ZodTypeDef = ZodTypeDef> =
  type ExtractParam (line 21) | type ExtractParam<Path, NextPart> = Path extends `:${infer Param}`
  type ExtractParams (line 25) | type ExtractParams<Path> = Path extends `${infer Segment}/${infer Rest}`
  type Payload (line 30) | type Payload =
  type Number (line 41) | type Number = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9
  type Version (line 43) | type Version = `v${
  type VersionRange (line 47) | type VersionRange =
  type Handler (line 55) | type Handler<
  function handler (line 73) | function handler<T>() {
  type BodylessHandler (line 108) | type BodylessHandler<
  function bodylessHandler (line 126) | function bodylessHandler<T>() {
  type HandlerOrSchema (line 158) | type HandlerOrSchema =

FILE: jwt.ts
  type Payload (line 6) | interface Payload {
  function importKey (line 23) | function importKey(key: string) {
  function sign (line 37) | async function sign<T extends Record<string, unknown> = {}>(
  function verify (line 59) | async function verify<T extends Record<string, unknown> = Payload>(

FILE: location_data.ts
  type CloudflareRequest (line 5) | type CloudflareRequest = Request & {
  class LocationData (line 14) | class LocationData {
    method constructor (line 17) | constructor(c: Context) {
    method city (line 26) | get city() {
    method region (line 41) | get region() {
    method country (line 54) | get country(): IncomingRequestCfProperties['country'] {
    method continent (line 70) | get continent(): IncomingRequestCfProperties['continent'] {
    method regionCode (line 86) | get regionCode(): IncomingRequestCfProperties['regionCode'] {
    method latitude (line 95) | get latitude(): IncomingRequestCfProperties['latitude'] {
    method longitude (line 110) | get longitude(): IncomingRequestCfProperties['longitude'] {
    method postalCode (line 125) | get postalCode(): IncomingRequestCfProperties['postalCode'] {
    method timezone (line 134) | get timezone(): IncomingRequestCfProperties['timezone'] {
    method datacenter (line 143) | get datacenter(): IncomingRequestCfProperties['colo'] {

FILE: oauth/client.ts
  type OAuthClient (line 6) | type OAuthClient = {

FILE: oauth/get_session_data.ts
  function getSessionData (line 13) | async function getSessionData(

FILE: oauth/get_session_id.ts
  function getSessionId (line 13) | async function getSessionId(c: Context): Promise<string | undefined> {

FILE: oauth/get_session_token.ts
  function getSessionToken (line 12) | async function getSessionToken(c: Context) {

FILE: oauth/handle_callback.ts
  function handleCallback (line 14) | async function handleCallback(

FILE: oauth/is_signed_in.ts
  function isSignedIn (line 11) | async function isSignedIn(c: Context): Promise<boolean> {

FILE: oauth/sign_in.ts
  function signIn (line 14) | async function signIn(

FILE: oauth/sign_out.ts
  function signOut (line 11) | async function signOut(c: Context) {

FILE: oauth/store.ts
  class OAuthStore (line 7) | class OAuthStore {
    method constructor (line 18) | constructor(
  method set (line 42) | async set(c, key, value, expiresAt) {
  method get (line 58) | async get(c, key) {
  method has (line 90) | async has(c, key) {
  method delete (line 124) | async delete(c, key) {
  constant REDIS (line 139) | let REDIS: Redis | undefined
  method set (line 148) | async set(c, key, value, expiresAt) {
  method get (line 161) | async get(c, key) {
  method has (line 172) | async has(c, key) {
  method delete (line 183) | async delete(c, key) {

FILE: oauth/types.ts
  type OAuthMethod (line 5) | type OAuthMethod =
  type OAuthSessionData (line 9) | type OAuthSessionData = {
  type OAuthSessionToken (line 29) | type OAuthSessionToken = {
  type OAuthSignInToken (line 35) | type OAuthSignInToken = {

FILE: otp.ts
  method secret (line 8) | secret(length = 64) {
  method token (line 17) | token(secret: string, timestamp?: number) {
  method uri (line 31) | uri(label: string, issuer: string, secret: string) {
  method validate (line 47) | validate(token: string, secret: string) {

FILE: render.ts
  function render (line 10) | function render(c: Context, Component: VNode) {
  class Renderer (line 30) | class Renderer {
    method constructor (line 33) | constructor(options?: Parameters<typeof defineConfig>[0]) {

FILE: request_context.ts
  type Static (line 11) | type Static<T extends ZodType> = T extends ZodType ? z.infer<T>
  class RequestContext (line 14) | class RequestContext<
    method constructor (line 30) | constructor(
    method gateway (line 51) | get gateway(): number {
    method ip (line 55) | get ip(): string {
    method method (line 65) | get method() {
    method param (line 72) | param<T extends keyof Params>(name: T): Params[T] {
    method raw (line 91) | get raw() {
    method body (line 98) | async body(options?: {
    method cookies (line 159) | get cookies(): [ValidatedCookies] extends [never] ? never
    method headers (line 200) | get headers(): [ValidatedHeaders] extends [never]
    method query (line 241) | get query(): [ValidatedQuery] extends [never] ? Record<string, unknown>
    method blob (line 292) | async blob(deadline = 2500) {
    method buffer (line 307) | async buffer(deadline = 2500) {
    method json (line 324) | async json(deadline = 2500): Promise<unknown> {
    method formData (line 339) | async formData(deadline = 2500) {
    method text (line 356) | async text(deadline = 2500) {
    method stream (line 369) | get stream() {

FILE: response_context.ts
  class ResponseContext (line 4) | class ResponseContext {
    method constructor (line 7) | constructor(__internal: {
    method body (line 15) | get body(): Exclude<Payload, void> | null {
    method body (line 19) | set body(data: Exclude<Payload, void> | null) {
    method bodySize (line 26) | get bodySize() {
    method code (line 73) | get code() {
    method code (line 77) | set code(code: number) {
    method setCookie (line 86) | setCookie(name: string, value: string, options?: {
    method deleteCookie (line 125) | deleteCookie(name: string, options?: { path?: string; domain?: string ...
    method header (line 135) | header(name: string, value: string | undefined) {
    method redirect (line 149) | redirect(destination: string, code = 307) {

FILE: send_mail.ts
  type MailContact (line 2) | type MailContact =
  function sendMail (line 11) | function sendMail(

FILE: test/extensions.test.ts
  method onPlugIn (line 9) | onPlugIn({ setRoute }) {
  method onRequest (line 14) | onRequest({ req }) {
  method onResponse (line 20) | onResponse({ c, prefix }) {
  method onRequest (line 28) | onRequest({ req }) {

FILE: test/preflight_mode.test.ts
  method notFound (line 58) | notFound() {
  method error (line 101) | error() {

FILE: test/routing/versioning.test.ts
  function get (line 15) | async function get(path: string) {
  function get2 (line 98) | async function get2(path: string) {
  function get (line 127) | async function get(path: string) {
Condensed preview — 73 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (175K chars).
[
  {
    "path": ".gitattributes",
    "chars": 19,
    "preview": "* text=auto eol=lf\n"
  },
  {
    "path": ".github/dependabot.yml",
    "chars": 117,
    "preview": "version: 2\nupdates:\n  - package-ecosystem: 'github-actions'\n    directory: '/'\n    schedule:\n      interval: 'daily'\n"
  },
  {
    "path": ".github/funding.yml",
    "chars": 26,
    "preview": "github: 'boywithkeyboard'\n"
  },
  {
    "path": ".github/workflows/check.yml",
    "chars": 650,
    "preview": "name: check\n\non:\n  - push\n  - pull_request\n\njobs:\n  check:\n    runs-on: ubuntu-latest\n\n    steps:\n      - uses: actions/"
  },
  {
    "path": ".github/workflows/publish.yml",
    "chars": 551,
    "preview": "name: publish\n\non:\n  workflow_dispatch:\n    inputs:\n      kind:\n        description: Kind of release\n        default: mi"
  },
  {
    "path": ".github/workflows/update.yml",
    "chars": 1147,
    "preview": "name: update\n\non:\n  schedule:\n    - cron: '0 0 * * *'\n  workflow_dispatch:\n\npermissions:\n  contents: write\n  pull-reques"
  },
  {
    "path": ".vscode/extensions.json",
    "chars": 122,
    "preview": "{\n  \"recommendations\": [\n    \"denoland.vscode-deno\",\n    \"gruntfuggly.todo-tree\",\n    \"wayou.vscode-todo-highlight\"\n  ]\n"
  },
  {
    "path": ".vscode/settings.json",
    "chars": 298,
    "preview": "{\n  \"deno.enable\": true,\n  \"deno.unstable\": true,\n  \"editor.defaultFormatter\": \"denoland.vscode-deno\",\n  \"[typescript]\":"
  },
  {
    "path": "base.ts",
    "chars": 1737,
    "preview": "// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.\nimport { bodylessHandler, handler, HandlerOrSche"
  },
  {
    "path": "cheetah.ts",
    "chars": 17727,
    "preview": "// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.\nimport { base, Method } from './base.ts'\nimport "
  },
  {
    "path": "cli/cmd/bundle/bundle.ts",
    "chars": 2207,
    "preview": "// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.\nimport { ensureFile } from 'std/fs/ensure_file.t"
  },
  {
    "path": "cli/cmd/bundle/mod.ts",
    "chars": 1346,
    "preview": "// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.\nimport { parse } from 'https://deno.land/std@0.2"
  },
  {
    "path": "cli/cmd/new/mod.ts",
    "chars": 2173,
    "preview": "// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.\nimport { ensureFile } from 'https://deno.land/st"
  },
  {
    "path": "cli/cmd/random/create_crypto_key.ts",
    "chars": 404,
    "preview": "// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.\nimport { encode } from 'std/encoding/base64.ts'\n"
  },
  {
    "path": "cli/cmd/random/create_jwt_secret.ts",
    "chars": 384,
    "preview": "// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.\nimport { encode } from 'std/encoding/base64.ts'\n"
  },
  {
    "path": "cli/cmd/random/mod.ts",
    "chars": 782,
    "preview": "// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.\nimport { Select } from 'cliffy'\nimport { gray, w"
  },
  {
    "path": "cli/cmd/serve/mod.ts",
    "chars": 1976,
    "preview": "// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.\nimport { keypress, KeyPressEvent } from 'cliffy/"
  },
  {
    "path": "cli/mod.ts",
    "chars": 637,
    "preview": "// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.\nimport { parse } from 'std/flags/mod.ts'\nimport "
  },
  {
    "path": "cli/utils.ts",
    "chars": 231,
    "preview": "// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.\nimport { brightRed, gray } from 'std/fmt/colors."
  },
  {
    "path": "codeowners",
    "chars": 19,
    "preview": "* @boywithkeyboard\n"
  },
  {
    "path": "collection.ts",
    "chars": 873,
    "preview": "// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.\nimport { base, Method } from './base.ts'\nimport "
  },
  {
    "path": "context.ts",
    "chars": 6445,
    "preview": "// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.\n/// <reference types='./env.d.ts' />\nimport { en"
  },
  {
    "path": "contributing.md",
    "chars": 590,
    "preview": "### Follow Conventional Commits\n\nThis repository enforces the\n[Conventional Commits](https://www.conventionalcommits.org"
  },
  {
    "path": "crypto.ts",
    "chars": 1601,
    "preview": "// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.\nimport { decode } from 'std/encoding/base64.ts'\n"
  },
  {
    "path": "deno.json",
    "chars": 1752,
    "preview": "{\n  \"compilerOptions\": {\n    \"jsxFactory\": \"h\",\n    \"jsxFragmentFactory\": \"Fragment\"\n  },\n  \"fmt\": {\n    \"semiColons\": f"
  },
  {
    "path": "env.d.ts",
    "chars": 73,
    "preview": "declare global {\n  type Variables = Record<string, unknown>\n}\n\nexport {}\n"
  },
  {
    "path": "ext/compress.ts",
    "chars": 2068,
    "preview": "// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.\nimport { compress as brotli } from 'brotli'\nimpo"
  },
  {
    "path": "ext/debug.ts",
    "chars": 880,
    "preview": "// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.\nimport { format } from 'std/fmt/bytes.ts'\nimport"
  },
  {
    "path": "ext/favicon.ts",
    "chars": 1028,
    "preview": "// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.\nimport { createExtension } from '../mod.ts'\n\nlet"
  },
  {
    "path": "ext/files.ts",
    "chars": 6321,
    "preview": "// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.\nimport { AwsClient } from 'aws4fetch'\nimport { j"
  },
  {
    "path": "ext/firewall.ts",
    "chars": 1762,
    "preview": "import { createExtension } from '../mod.ts'\n\nlet VPN_LIST: string[] = []\nlet DATACENTER_LIST: string[] = []\nlet LAST_UPD"
  },
  {
    "path": "ext/helmet.ts",
    "chars": 5016,
    "preview": "// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.\nimport { createExtension } from '../mod.ts'\n\n/**"
  },
  {
    "path": "ext/pretty.ts",
    "chars": 1371,
    "preview": "// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.\nimport { createExtension } from '../mod.ts'\n\nfun"
  },
  {
    "path": "extensions.ts",
    "chars": 4465,
    "preview": "// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.\nimport { ZodString, ZodUnion } from 'zod'\nimport"
  },
  {
    "path": "handler.ts",
    "chars": 4285,
    "preview": "// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.\nimport {\n  ZodObject,\n  ZodRecord,\n  ZodString,\n"
  },
  {
    "path": "jwt.ts",
    "chars": 1836,
    "preview": "// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.\nimport * as Jwt from 'djwt'\nimport { decode } fr"
  },
  {
    "path": "license",
    "chars": 10756,
    "preview": "                                 Apache License\n                           Version 2.0, January 2004\n                   "
  },
  {
    "path": "location_data.ts",
    "chars": 4282,
    "preview": "// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.\nimport { IncomingRequestCfProperties } from 'wor"
  },
  {
    "path": "mod.ts",
    "chars": 617,
    "preview": "// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.\nexport { cheetah as default } from './cheetah.ts"
  },
  {
    "path": "oauth/client.ts",
    "chars": 565,
    "preview": "// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.\nimport { GitHub as githubPreset, Google as googl"
  },
  {
    "path": "oauth/get_session_data.ts",
    "chars": 1313,
    "preview": "// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.\nimport { getCookies } from 'std/http/cookie.ts'\n"
  },
  {
    "path": "oauth/get_session_id.ts",
    "chars": 1264,
    "preview": "// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.\nimport { getCookies } from 'std/http/cookie.ts'\n"
  },
  {
    "path": "oauth/get_session_token.ts",
    "chars": 585,
    "preview": "// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.\nimport { getCookies } from 'std/http/cookie.ts'\n"
  },
  {
    "path": "oauth/handle_callback.ts",
    "chars": 2974,
    "preview": "// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.\nimport { getNormalizedUser, getToken, getUser } "
  },
  {
    "path": "oauth/is_signed_in.ts",
    "chars": 357,
    "preview": "// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.\nimport { Context } from '../context.ts'\nimport {"
  },
  {
    "path": "oauth/mod.ts",
    "chars": 623,
    "preview": "// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.\nexport { GitHub, Google } from './client.ts'\nexp"
  },
  {
    "path": "oauth/sign_in.ts",
    "chars": 1064,
    "preview": "// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.\nimport { createAuthorizeUrl } from 'authenticus'"
  },
  {
    "path": "oauth/sign_out.ts",
    "chars": 854,
    "preview": "// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.\nimport { Context } from '../context.ts'\nimport {"
  },
  {
    "path": "oauth/store.ts",
    "chars": 4213,
    "preview": "// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.\nimport { Redis } from 'upstash'\nimport { KVNames"
  },
  {
    "path": "oauth/types.ts",
    "chars": 867,
    "preview": "// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.\nimport { UserAgent } from 'std/http/user_agent.t"
  },
  {
    "path": "otp.ts",
    "chars": 1253,
    "preview": "// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.\nimport * as OTP from 'otpauth'\n\nexport const otp"
  },
  {
    "path": "readme.md",
    "chars": 1865,
    "preview": "<div align='center'>\n  <img src='https://cheetah.mod.land/cheetah.svg' width='128px' />\n  <br>\n  <br>\n  <h1>cheetah</h1>"
  },
  {
    "path": "render.ts",
    "chars": 1446,
    "preview": "// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.\nimport { VNode } from 'preact'\nimport renderToSt"
  },
  {
    "path": "request_context.ts",
    "chars": 8802,
    "preview": "// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.\nimport {\n  deadline as resolveWithDeadline,\n  De"
  },
  {
    "path": "response_context.ts",
    "chars": 3090,
    "preview": "// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.\nimport { Payload } from './handler.ts'\n\nexport c"
  },
  {
    "path": "send_mail.ts",
    "chars": 2510,
    "preview": "// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.\ntype MailContact =\n  | { name?: string; email: s"
  },
  {
    "path": "test/context.test.ts",
    "chars": 477,
    "preview": "// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.\nimport { assertEquals } from 'std/assert/mod.ts'"
  },
  {
    "path": "test/cors.test.ts",
    "chars": 1099,
    "preview": "// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.\nimport { assertEquals } from 'std/assert/mod.ts'"
  },
  {
    "path": "test/exception.test.ts",
    "chars": 42,
    "preview": "// TODO create test suite for c.exception\n"
  },
  {
    "path": "test/ext/favicon.test.ts",
    "chars": 615,
    "preview": "// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.\nimport { assertEquals } from 'std/assert/mod.ts'"
  },
  {
    "path": "test/ext/files.test.ts",
    "chars": 1578,
    "preview": "// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.\nimport { assertEquals } from 'std/assert/mod.ts'"
  },
  {
    "path": "test/ext/firewall.test.ts",
    "chars": 1205,
    "preview": "import { assertEquals } from 'std/assert/mod.ts'\nimport { firewall } from '../../ext/firewall.ts'\nimport cheetah from '."
  },
  {
    "path": "test/ext/pretty.test.ts",
    "chars": 1540,
    "preview": "// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.\nimport { assertEquals } from 'std/assert/mod.ts'"
  },
  {
    "path": "test/extensions.test.ts",
    "chars": 1876,
    "preview": "// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.\nimport { assertEquals } from 'std/assert/mod.ts'"
  },
  {
    "path": "test/jwt.test.ts",
    "chars": 1014,
    "preview": "// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.\nimport { assertEquals } from 'std/assert/mod.ts'"
  },
  {
    "path": "test/many_handlers.test.ts",
    "chars": 1066,
    "preview": "// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.\nimport { assertEquals } from 'std/assert/mod.ts'"
  },
  {
    "path": "test/preflight_mode.test.ts",
    "chars": 3062,
    "preview": "// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.\nimport { assertEquals } from 'std/assert/mod.ts'"
  },
  {
    "path": "test/render.test.tsx",
    "chars": 4823,
    "preview": "// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.\n/** @jsx h */\nimport { DOMParser } from 'dom'\nim"
  },
  {
    "path": "test/request.test.ts",
    "chars": 6879,
    "preview": "// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.\nimport { assertEquals } from 'std/assert/mod.ts'"
  },
  {
    "path": "test/response.test.ts",
    "chars": 4031,
    "preview": "// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.\nimport { assertEquals } from 'std/assert/mod.ts'"
  },
  {
    "path": "test/routing/versioning.test.ts",
    "chars": 6668,
    "preview": "import { assertEquals } from 'std/assert/mod.ts'\nimport { cheetah } from '../../cheetah.ts'\n\nDeno.test('versioning', asy"
  },
  {
    "path": "test/routing.test.ts",
    "chars": 1235,
    "preview": "// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.\nimport { assertEquals } from 'std/assert/mod.ts'"
  },
  {
    "path": "test/validation.test.ts",
    "chars": 5100,
    "preview": "// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.\nimport { assertEquals } from 'std/assert/mod.ts'"
  }
]

About this extraction

This page contains the full source code of the azurystudio/cheetah GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 73 files (160.7 KB), approximately 44.5k tokens, and a symbol index with 187 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!