[
  {
    "path": ".gitattributes",
    "content": "* text=auto eol=lf\n"
  },
  {
    "path": ".github/dependabot.yml",
    "content": "version: 2\nupdates:\n  - package-ecosystem: 'github-actions'\n    directory: '/'\n    schedule:\n      interval: 'daily'\n"
  },
  {
    "path": ".github/funding.yml",
    "content": "github: 'boywithkeyboard'\n"
  },
  {
    "path": ".github/workflows/check.yml",
    "content": "name: check\n\non:\n  - push\n  - pull_request\n\njobs:\n  check:\n    runs-on: ubuntu-latest\n\n    steps:\n      - uses: actions/checkout@v4\n\n      - uses: actions/cache@v3\n        with:\n          path: |\n            ~/.cache/deno\n            ~/.deno\n          key: ${{ runner.os }}-deno-${{ hashFiles('**/*') }}\n          restore-keys: |\n            ${{ runner.os }}-deno-\n\n      - name: Setup Deno\n        uses: denoland/setup-deno@v1\n        with:\n          deno-version: v1.x\n\n      - name: Run deno fmt\n        run: deno fmt --check\n\n      - name: Run deno lint\n        run: deno lint\n\n      - name: Run deno test\n        run: deno task test --fail-fast\n"
  },
  {
    "path": ".github/workflows/publish.yml",
    "content": "name: publish\n\non:\n  workflow_dispatch:\n    inputs:\n      kind:\n        description: Kind of release\n        default: minor\n        type: choice\n        options:\n        - prepatch\n        - patch\n        - preminor\n        - minor\n        - premajor\n        - major\n        required: true\n\njobs:\n  publish:\n    runs-on: ubuntu-latest\n\n    steps:\n      - uses: actions/checkout@v4\n\n      - name: Publish update\n        uses: boywithkeyboard/publisher@v2\n        with:\n          kind: ${{github.event.inputs.kind}}\n          mention_contributors: true\n"
  },
  {
    "path": ".github/workflows/update.yml",
    "content": "name: update\n\non:\n  schedule:\n    - cron: '0 0 * * *'\n  workflow_dispatch:\n\npermissions:\n  contents: write\n  pull-requests: write\n\njobs:\n  update:\n    runs-on: ubuntu-latest\n\n    steps:\n      - uses: actions/checkout@v4\n\n      - name: Setup Deno\n        uses: denoland/setup-deno@v1\n        with:\n          deno-version: v1.x\n\n      - name: Run update\n        run: |\n          deno run -Ar https://deno.land/x/update/mod.ts -c\n          CHANGELOG=$(cat updates_changelog.md)\n          echo \"CHANGELOG<<EOF\" >> $GITHUB_ENV\n          echo \"$CHANGELOG\" >> $GITHUB_ENV\n          echo \"EOF\" >> $GITHUB_ENV\n          rm updates_changelog.md\n\n      - name: Create pull request\n        uses: peter-evans/create-pull-request@v5\n        with:\n          title: 'refactor: update deps'\n          author: 'github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>'\n          committer: 'github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>'\n          commit-message: 'refactor: update deps'\n          body: '${{ env.CHANGELOG }}'\n          labels: 'deps'\n          delete-branch: true\n          branch: 'refactor/deps'\n"
  },
  {
    "path": ".vscode/extensions.json",
    "content": "{\n  \"recommendations\": [\n    \"denoland.vscode-deno\",\n    \"gruntfuggly.todo-tree\",\n    \"wayou.vscode-todo-highlight\"\n  ]\n}\n"
  },
  {
    "path": ".vscode/settings.json",
    "content": "{\n  \"deno.enable\": true,\n  \"deno.unstable\": true,\n  \"editor.defaultFormatter\": \"denoland.vscode-deno\",\n  \"[typescript]\": {\n    \"editor.defaultFormatter\": \"denoland.vscode-deno\"\n  },\n  \"[typescriptreact]\": {\n    \"editor.defaultFormatter\": \"denoland.vscode-deno\"\n  },\n  \"editor.formatOnSave\": true\n}\n"
  },
  {
    "path": "base.ts",
    "content": "// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.\nimport { bodylessHandler, handler, HandlerOrSchema } from './handler.ts'\n\nexport type Method =\n  | 'delete'\n  | 'get'\n  | 'head'\n  | 'patch'\n  | 'post'\n  | 'put'\n\nexport const METHODS: Method[] = [\n  'delete',\n  'get',\n  'head',\n  'patch',\n  'post',\n  'put',\n]\n\nexport function base<T>(): {\n  new (\n    addRoute: (\n      method: Uppercase<Method>,\n      pathname: string,\n      handlers: HandlerOrSchema[],\n    ) => unknown,\n  ):\n    & {\n      [\n        M in (\n          | 'delete'\n          | 'patch'\n          | 'post'\n          | 'put'\n        )\n      ]: ReturnType<typeof handler<T>>\n    }\n    & {\n      [\n        M in (\n          | 'get'\n          | 'head'\n        )\n      ]: ReturnType<typeof bodylessHandler<T>>\n    }\n} {\n  return class {\n    #a\n\n    constructor(\n      addRoute: (\n        method: Uppercase<Method>,\n        pathname: string,\n        handlers: HandlerOrSchema[],\n      ) => unknown,\n    ) {\n      this.#a = addRoute\n    }\n\n    delete(pathname: string, ...handlers: HandlerOrSchema[]) {\n      return this.#a('DELETE', pathname, handlers)\n    }\n\n    get(pathname: string, ...handlers: HandlerOrSchema[]) {\n      return this.#a('GET', pathname, handlers)\n    }\n\n    head(pathname: string, ...handlers: HandlerOrSchema[]) {\n      return this.#a('HEAD', pathname, handlers)\n    }\n\n    patch(pathname: string, ...handlers: HandlerOrSchema[]) {\n      return this.#a('PATCH', pathname, handlers)\n    }\n\n    post(pathname: string, ...handlers: HandlerOrSchema[]) {\n      return this.#a('POST', pathname, handlers)\n    }\n\n    put(pathname: string, ...handlers: HandlerOrSchema[]) {\n      return this.#a('PUT', pathname, handlers)\n    }\n  } as never\n}\n"
  },
  {
    "path": "cheetah.ts",
    "content": "// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.\nimport { base, Method } from './base.ts'\nimport { Collection } from './collection.ts'\nimport { Context, Exception } from './context.ts'\nimport { Extension, validExtension } from './extensions.ts'\nimport {\n  Handler,\n  HandlerOrSchema,\n  Payload,\n  Version,\n  VersionRange,\n} from './handler.ts'\nimport { OAuthStore } from './oauth/mod.ts'\nimport { OAuthSessionData } from './oauth/types.ts'\nimport { ResponseContext } from './response_context.ts'\n\nexport type AppContext = {\n  debugging: boolean\n  gateway?: number\n  env: Record<string, unknown> | undefined\n  ip: string\n  proxy: AppConfig['proxy']\n  routes: cheetah['routes']\n  runtime:\n    | 'cloudflare'\n    | 'deno'\n  request: {\n    pathname: string\n    querystring?: string\n  }\n  oauth: AppConfig['oauth']\n  versioning: AppConfig['versioning']\n  caching: AppConfig['cache']\n}\n\nexport type AppConfig = {\n  /**\n   * A prefix for all routes, e.g. `/api`.\n   *\n   * @default '/'\n   */\n  base?: `/${string}`\n\n  /**\n   * Enable Cross-Origin Resource Sharing (CORS) for your app by setting a origin, e.g. `*`.\n   */\n  cors?: string\n\n  /**\n   * If enabled, cheetah will attempt to find the matching `.get()` handler for an incoming HEAD request. Your existing `.head()` handlers won't be impacted.\n   *\n   * @default false\n   * @since v0.11\n   */\n  preflight?: boolean\n\n  /**\n   * If you're using Cloudflare as a proxy, you should confirm it with this setting in order to unleash the full potential of cheetah.\n   *\n   * @default undefined\n   */\n  proxy?: 'cloudflare'\n\n  /**\n   * Set a custom error handler.\n   */\n  error?: (error: unknown, request: Request) => Response | Promise<Response>\n\n  /**\n   * Set a custom 404 handler.\n   */\n  notFound?: (request: Request) => Response | Promise<Response>\n\n  oauth?: {\n    store: OAuthStore\n    cookie?: Parameters<ResponseContext['setCookie']>[2]\n    onSignIn?: (\n      c: Context,\n      data: OAuthSessionData,\n    ) => Promise<unknown> | unknown\n    onSignOut?: (\n      c: Context,\n      identifier: string,\n    ) => Promise<unknown> | unknown\n  }\n\n  /**\n   * If you enable debug mode, all errors will get logged to the console.\n   *\n   * @since v1.3\n   */\n  debug?: boolean\n\n  versioning?:\n    & (\n      | {\n        type: 'uri'\n      }\n      | {\n        type: 'header'\n        header: string\n      }\n    )\n    & {\n      current: Version\n    }\n\n  cache?: {\n    /** A unique name for your cache. */\n    name: string\n  }\n}\n\nexport class cheetah extends base<cheetah>() {\n  #base\n  #cors\n  #error\n  #extensions: Set<[string, Extension]>\n  #notFound\n  #preflight\n  #proxy\n  #routes: Set<[Uppercase<Method>, string, RegExp, HandlerOrSchema[]]>\n  #runtime: 'deno' | 'cloudflare'\n  #onPlugIn\n  #oauth\n  #debug\n  #versioning\n  #cache\n\n  constructor({\n    base,\n    cors,\n    preflight = false,\n    proxy,\n    error,\n    notFound,\n    oauth,\n    debug = false,\n    versioning,\n    cache,\n  }: AppConfig = {}) {\n    super((method, pathname, handlers) => {\n      pathname = this.#base ? this.#base + pathname : pathname\n\n      this.#routes.add([\n        method,\n        pathname,\n        RegExp(`^${\n          (pathname\n            .replace(/\\/+(\\/|$)/g, '$1'))\n            .replace(/(\\/?\\.?):(\\w+)\\+/g, '($1(?<$2>*))')\n            .replace(/(\\/?\\.?):(\\w+)/g, '($1(?<$2>[^$1/]+?))')\n            .replace(/\\./g, '\\\\.')\n            .replace(/(\\/?)\\*/g, '($1.*)?')\n        }/*$`),\n        handlers,\n      ])\n\n      return this\n    })\n\n    this.#base = base === '/' ? undefined : base\n    this.#cors = cors\n    this.#error = error\n    this.#extensions = new Set()\n    this.#notFound = notFound\n    this.#preflight = preflight\n    this.#proxy = proxy\n    this.#routes = new Set()\n    this.#runtime = typeof globalThis?.Deno?.serve !== 'function'\n      ? 'cloudflare'\n      : 'deno'\n    this.#onPlugIn = false\n    this.#oauth = oauth\n    this.#debug = debug\n    this.#versioning = versioning\n    this.#cache = cache\n  }\n\n  /* use ---------------------------------------------------------------------- */\n\n  // deno-lint-ignore no-explicit-any\n  use<C extends Collection>(...extensions: Extension<any>[]): this\n  use<C extends Collection>(\n    prefix: `/${string}`,\n    // deno-lint-ignore no-explicit-any\n    ...extensions: Extension<any>[]\n  ): this\n  use<C extends Collection>(\n    prefix: `/${string}`,\n    collection: C,\n    // deno-lint-ignore no-explicit-any\n    ...extensions: Extension<any>[]\n  ): this\n\n  use<C extends Collection>(\n    // deno-lint-ignore no-explicit-any\n    ...elements: (`/${string}` | C | Extension<any>)[]\n  ) {\n    let pre\n\n    for (const e of elements) {\n      if (typeof e === 'string') { // prefix\n        pre = e\n      } else if (e instanceof Collection) { // collection\n        if (!pre || pre === '/') {\n          pre = ''\n        }\n\n        for (const r of e.routes.values()) {\n          let pathname = r[1]\n\n          if (pathname === '/') {\n            pathname = ''\n          }\n\n          pathname = this.#base ? this.#base + pre + pathname : pre + pathname\n\n          this.#routes.add([\n            r[0],\n            pathname,\n            RegExp(\n              `^${\n                (pathname\n                  .replace(/\\/+(\\/|$)/g, '$1'))\n                  .replace(/(\\/?\\.?):(\\w+)\\+/g, '($1(?<$2>*))')\n                  .replace(/(\\/?\\.?):(\\w+)/g, '($1(?<$2>[^$1/]+?))')\n                  .replace(/\\./g, '\\\\.')\n                  .replace(/(\\/?)\\*/g, '($1.*)?')\n              }/*$`,\n            ),\n            r[2],\n          ])\n        }\n      } else if (validExtension(e)) { // extension\n        if (!pre) {\n          pre = '*'\n        }\n\n        // @ts-ignore:\n        this.#extensions.add([pre, e])\n      }\n    }\n\n    return this\n  }\n\n  get routes() {\n    return this.#routes\n  }\n\n  /* router ------------------------------------------------------------------- */\n\n  #parseVersion(headers: Headers, pathname: string) {\n    if (!this.#versioning) { // for typescript\n      throw new Error('Versioning not configured!')\n    }\n\n    const regex = /^v[1-9][0-9]?$|^100$/\n\n    if (this.#versioning.type === 'uri') {\n      const arr = pathname.replace('/', '').split('/')\n\n      if (this.#base) {\n        if (regex.test(arr[1])) {\n          const version = arr[1]\n\n          arr.splice(1, 1)\n\n          return { version, pathname: '/' + arr.join('/') }\n        }\n      } else {\n        if (regex.test(arr[0])) {\n          const version = arr[0]\n\n          arr.shift()\n\n          return { version, pathname: '/' + arr.join('/') }\n        }\n      }\n\n      return { version: this.#versioning.current, pathname }\n    }\n\n    const header = headers.get(this.#versioning.header)\n\n    if (\n      this.#versioning.type === 'header' && header !== null &&\n      regex.test(header)\n    ) {\n      return { version: header, pathname }\n    }\n\n    return { version: this.#versioning.current, pathname }\n  }\n\n  #match(request: Request, p: string) {\n    for (const r of this.#routes.values()) {\n      if (\n        request.method === r[0] ||\n        request.method === 'OPTIONS' ||\n        this.#preflight && request.method === 'HEAD' && r[0] === 'GET'\n      ) {\n        if (this.#versioning) {\n          const { pathname, version } = this.#parseVersion(request.headers, p)\n\n          if (\n            parseInt(version.replace('v', '')) >\n              parseInt(this.#versioning.current.replace('v', ''))\n          ) {\n            break\n          }\n\n          const options = typeof r[3][0] !== 'function' ? r[3][0] : null\n\n          if (options?.gateway !== undefined) {\n            const result = pathname.match(r[2])\n\n            if (!result) {\n              continue\n            }\n\n            const gateway = isVersionWithinRange(\n              version as Version,\n              options.gateway as VersionRange,\n            )\n\n            if (!gateway) {\n              break\n            }\n\n            return {\n              handlers: r[3],\n              params: result.groups ?? {},\n              gateway,\n            }\n          } else {\n            const result = pathname.match(r[2])\n\n            if (!result) {\n              continue\n            }\n\n            return {\n              handlers: r[3],\n              params: result.groups ?? {},\n              gateway: parseInt(version.replace('v', '')),\n            }\n          }\n        } else {\n          const result = p.match(r[2])\n\n          if (!result) {\n            continue\n          }\n\n          return {\n            handlers: r[3],\n            params: result.groups ?? {},\n            gateway: undefined,\n          }\n        }\n      }\n    }\n\n    return null\n  }\n\n  /* fetch -------------------------------------------------------------------- */\n\n  fetch = async (\n    req: Request,\n    data: Record<string, unknown> | Deno.ServeHandlerInfo = {},\n    context?: {\n      waitUntil: (promise: Promise<unknown>) => void\n    },\n  ): Promise<Response> => {\n    try {\n      const ip = data?.remoteAddr && this.#runtime === 'deno'\n        ? (data as Deno.ServeHandlerInfo).remoteAddr\n          .hostname\n        : req.headers.get('cf-connecting-ip') as string\n\n      const parts = req.url.split('?')\n\n      parts[0] = parts[0].slice(8)\n\n      const __app: AppContext = {\n        env: data as Record<string, unknown>,\n        ip,\n        proxy: this.#proxy,\n        request: {\n          pathname: parts[0].substring(parts[0].indexOf('/')),\n          querystring: parts[1],\n        },\n        routes: this.#routes,\n        runtime: this.#runtime,\n        oauth: this.#oauth,\n        versioning: this.#versioning,\n        gateway: -1,\n        debugging: this.#debug,\n        caching: this.#cache,\n      }\n\n      if (this.#extensions.size > 0) {\n        let body: Response | void = undefined\n\n        for (const e of this.#extensions.values()) {\n          if (!this.#onPlugIn && e[1].onPlugIn !== undefined) {\n            await e[1].onPlugIn({\n              prefix: e[0],\n              env: __app.env,\n              routes: this.#routes,\n              runtime: this.#runtime,\n              settings: e[1].__config,\n              setRoute: (method, pathname, ...handlers) => {\n                this.#routes.add([\n                  method.toUpperCase() as Uppercase<Method>,\n                  pathname,\n                  RegExp(\n                    `^${\n                      (pathname\n                        .replace(/\\/+(\\/|$)/g, '$1'))\n                        .replace(/(\\/?\\.?):(\\w+)\\+/g, '($1(?<$2>*))')\n                        .replace(/(\\/?\\.?):(\\w+)/g, '($1(?<$2>[^$1/]+?))')\n                        .replace(/\\./g, '\\\\.')\n                        .replace(/(\\/?)\\*/g, '($1.*)?')\n                    }/*$`,\n                  ),\n                  // @ts-ignore:\n                  handlers,\n                ])\n              },\n            })\n          }\n\n          if (\n            e[0] !== '*' &&\n            __app.request.pathname.indexOf(e[0]) !== 0\n          ) {\n            continue\n          }\n\n          if (e[1].onRequest !== undefined) {\n            const result = await e[1].onRequest({\n              prefix: e[0],\n              app: __app,\n              req,\n              _: e[1].__config,\n            })\n\n            if (result !== undefined) {\n              body = result\n            }\n          }\n        }\n\n        if (!this.#onPlugIn) {\n          this.#onPlugIn = true\n        }\n\n        if (body !== undefined) {\n          return body\n        }\n      }\n\n      const route = this.#match(\n        req,\n        __app.request.pathname,\n      )\n\n      __app.gateway = route?.gateway ?? -1\n\n      if (!route) {\n        if (!this.#notFound) {\n          throw new Exception('Not Found', undefined, 404)\n        }\n\n        if (req.method !== 'HEAD') {\n          return await this.#notFound(req)\n        }\n\n        const response = await this.#notFound(req)\n\n        return new Response(null, {\n          headers: response.headers,\n          status: response.status,\n          statusText: response.statusText,\n        })\n      }\n\n      const response = await this.#handle(\n        __app,\n        req,\n        context?.waitUntil ??\n          ((promise: Promise<unknown>) => {\n            // deno-fmt-ignore-line\n            (async () => await promise)()\n          }),\n        route.params,\n        route.handlers,\n      )\n\n      if (req.method === 'HEAD') {\n        return new Response(null, {\n          headers: response.headers,\n          status: response.status,\n          statusText: response.statusText,\n        })\n      }\n\n      return response\n    } catch (err) {\n      let res: Response\n\n      if (err instanceof Exception) {\n        res = err.response(req)\n      } else {\n        if (this.#debug) {\n          console.error(err)\n        }\n\n        if (this.#error) {\n          res = await this.#error(err, req)\n        } else {\n          res = new Exception('Something Went Wrong', undefined, 500).response(\n            req,\n          )\n        }\n      }\n\n      if (req.method === 'HEAD') {\n        return new Response(null, {\n          headers: res.headers,\n          status: res.status,\n          statusText: res.statusText,\n        })\n      }\n\n      return res\n    }\n  }\n\n  /* handle ------------------------------------------------------------------- */\n\n  async #handle(\n    __app: AppContext,\n    r: Request,\n    waitUntil: (promise: Promise<unknown>) => void,\n    p: Record<string, string | undefined>,\n    handlers: HandlerOrSchema[],\n  ) {\n    const o = typeof handlers[0] !== 'function' ? handlers[0] : null\n\n    // preflight cors request\n\n    if (\n      r.method === 'OPTIONS' &&\n      r.headers.has('origin') &&\n      r.headers.has('access-control-request-method')\n    ) {\n      return new Response(null, {\n        status: 204,\n        headers: {\n          ...((this.#cors || o?.cors) &&\n            { 'access-control-allow-origin': o?.cors ?? this.#cors }),\n          'access-control-allow-methods': '*',\n          'access-control-allow-headers':\n            r.headers.get('access-control-request-headers') ?? '*',\n          'access-control-allow-credentials': 'false',\n          'access-control-max-age': '600',\n        },\n      })\n    }\n\n    // construct context\n\n    const $: {\n      b: Exclude<Payload, void> | null\n      c: number\n      h: Headers\n    } = {\n      b: null,\n      c: 200,\n      h: new Headers({\n        ...(this.#cors && {\n          'access-control-allow-origin': this.#cors,\n        }),\n      }),\n    }\n\n    const context = new Context(\n      __app,\n      $,\n      p,\n      r,\n      o,\n      waitUntil,\n    )\n\n    // handle request\n\n    const len = handlers.length\n\n    let next = true\n\n    for (let i = 0; i < len; ++i) {\n      if (typeof handlers[i] !== 'function') {\n        continue\n      }\n\n      if (!next) {\n        break\n      }\n\n      next = false\n\n      const result = await (handlers[i] as Handler<unknown>)(\n        context,\n        () => {\n          next = true\n        },\n      )\n\n      if (result) {\n        $.b = result\n      }\n    }\n\n    // construct response\n\n    if ($.c.toString().indexOf('3') === 0) {\n      return new Response(null, {\n        headers: $.h,\n        status: $.c,\n      })\n    }\n\n    if (!$.b) {\n      return new Response(null, {\n        headers: $.h,\n        status: $.c,\n      })\n    }\n\n    for (const e of this.#extensions.values()) {\n      if (\n        e[0] !== '*' &&\n        __app.request.pathname.indexOf(e[0]) !== 0\n      ) {\n        continue\n      }\n\n      const { onResponse } = e[1]\n\n      if (onResponse !== undefined) {\n        onResponse({\n          prefix: e[0],\n          app: __app,\n          c: context,\n          _: e[1].__config,\n        })\n      }\n    }\n\n    switch ($.b.constructor.name) {\n      case 'String': {\n        if (!$.h.has('content-type')) {\n          $.h.set('content-type', 'text/plain; charset=utf-8')\n        }\n\n        break\n      }\n\n      case 'Object': {\n        try {\n          if ((($.b as unknown) as { code: number }).code) {\n            $.c = (($.b as unknown) as { code: number }).code\n          }\n\n          $.b = JSON.stringify($.b)\n\n          if (!$.h.has('content-type')) {\n            $.h.set('content-type', 'application/json; charset=utf-8')\n          }\n        } catch (err) {\n          console.log(err)\n        }\n\n        break\n      }\n\n      case 'Array': {\n        $.b = JSON.stringify($.b)\n\n        if (!$.h.has('content-type')) {\n          $.h.set('content-type', 'application/json; charset=utf-8')\n        }\n\n        break\n      }\n\n      default:\n        break\n    }\n\n    return new Response($.b as BodyInit, {\n      headers: $.h,\n      status: $.c,\n    })\n  }\n\n  /* serve -------------------------------------------------------------------- */\n\n  serve({\n    hostname,\n    port,\n  }: {\n    hostname?: string\n    port?: number\n  } = {}) {\n    return Deno.serve({\n      hostname,\n      port,\n    }, (request, data) => {\n      return this.fetch(request, data)\n    }).finished\n  }\n}\n\nfunction isVersionWithinRange(\n  version: Version,\n  r: VersionRange,\n): number | undefined {\n  const v = parseInt(version.replace('v', ''))\n\n  if (parseInt(r.replace('v', '')) === v) {\n    return v\n  }\n\n  if (r.startsWith('v') && r.includes('...')) { // from (min) ... to (max)\n    const from = parseInt(r.split('...')[0].replace('v', ''))\n    const to = parseInt(r.split('...')[1].replace('v', ''))\n\n    return v >= from && v <= to ? v : undefined\n  } else if (r.startsWith('> ')) {\n    return v > parseInt(r.replace('> v', '')) ? v : undefined\n  } else if (r.startsWith('< ')) {\n    return v < parseInt(r.replace('< v', '')) ? v : undefined\n  } else if (r.startsWith('>= ')) {\n    return v >= parseInt(r.replace('>= v', '')) ? v : undefined\n  } else if (r.startsWith('<= ')) {\n    return v <= parseInt(r.replace('<= v', '')) ? v : undefined\n  }\n\n  return undefined\n}\n"
  },
  {
    "path": "cli/cmd/bundle/bundle.ts",
    "content": "// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.\nimport { ensureFile } from 'std/fs/ensure_file.ts'\nimport { join } from 'std/path/mod.ts'\nimport * as esbuild from 'esbuild'\n\nexport async function bundle({\n  input = './mod.ts',\n  output = './mod.js',\n  banner = '// deno-fmt-ignore-file\\n// deno-lint-ignore-file',\n  target = 'es2022',\n  cwd = Deno.cwd(),\n  runtime: _ = 'cloudflare',\n}: {\n  /**\n   * @default './mod.ts'\n   */\n  input?: string\n  /**\n   * @default './mod.js'\n   */\n  output?: string\n  /**\n   * @default '// deno-fmt-ignore-file\\n// deno-lint-ignore-file'\n   */\n  banner?: string\n  /**\n   * @default 'es2022'\n   */\n  target?:\n    | 'es2015'\n    | 'es2016'\n    | 'es2017'\n    | 'es2018'\n    | 'es2019'\n    | 'es2020'\n    | 'es2021'\n    | 'es2022'\n  /**\n   * @default Deno.cwd()\n   */\n  cwd?: string\n  /**\n   * @default 'deno'\n   */\n  runtime?:\n    | 'cloudflare'\n    | 'deno'\n}) {\n  const hasImportMap = async (path: string) => {\n    try {\n      const content = await Deno.readTextFile(join(cwd, path))\n\n      if (!content) {\n        return false\n      }\n\n      return JSON.parse(content).imports !== undefined\n    } catch (_) {\n      return false\n    }\n  }\n\n  let options: string[] = []\n\n  if (await hasImportMap('deno.json')) {\n    options = ['--config', 'deno.json']\n  } else if (await hasImportMap('deno.jsonc')) {\n    options = ['--config', 'deno.jsonc']\n  } else if (await hasImportMap('import_map.json')) {\n    options = ['--import-map', 'deno.json']\n  } else if (await hasImportMap('importMap.json')) {\n    options = ['--import-map', 'importMap.json']\n  } else if (await hasImportMap('imports.json')) {\n    options = ['--import-map', 'imports.json']\n  }\n\n  ensureFile(output)\n\n  const cmd = new Deno.Command('deno', {\n    args: ['bundle', '-q', '--unstable', ...options, input, output],\n    cwd,\n  })\n\n  await cmd.output()\n\n  await esbuild.build({\n    entryPoints: [output],\n    bundle: true,\n    minify: true,\n    format: 'esm',\n    allowOverwrite: true,\n    target,\n    banner: {\n      js: banner,\n    },\n    outfile: output,\n    absWorkingDir: cwd,\n  })\n\n  esbuild.stop()\n\n  return {\n    outputSize: (await Deno.readTextFile(join(cwd, output))).length,\n  }\n}\n"
  },
  {
    "path": "cli/cmd/bundle/mod.ts",
    "content": "// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.\nimport { parse } from 'https://deno.land/std@0.203.0/flags/mod.ts'\nimport {\n  brightGreen,\n  brightRed,\n  brightYellow,\n  gray,\n} from 'https://deno.land/std@0.203.0/fmt/colors.ts'\nimport byte from 'https://deno.land/x/byte@v3.3.0/byte.ts'\nimport { logError } from '../../utils.ts'\nimport { bundle } from './bundle.ts'\n\nexport async function bundleCommand(args: ReturnType<typeof parse>) {\n  const { _, target, runtime } = args\n\n  const input = _[1] && typeof _[1] === 'string' ? _[1] : undefined\n  const output = _[2] && typeof _[2] === 'string' ? _[2] : undefined\n\n  try {\n    const { outputSize } = await bundle({\n      input,\n      output,\n      // @ts-ignore:\n      target: typeof target === 'string' ? target : undefined,\n      // @ts-ignore:\n      runtime: typeof runtime === 'string' ? runtime : undefined,\n    })\n\n    console.info(\n      gray(\n        `file size - ${\n          outputSize < 1_000_000\n            ? brightGreen(byte(outputSize))\n            : outputSize < 5_000_000\n            ? brightYellow(byte(outputSize))\n            : brightRed(byte(outputSize))\n        }`,\n      ),\n    )\n  } catch (err) {\n    if (err instanceof Error) {\n      logError(err.message)\n    } else {\n      logError('something went wrong trying to bundle your app.')\n    }\n  }\n}\n"
  },
  {
    "path": "cli/cmd/new/mod.ts",
    "content": "// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.\nimport { ensureFile } from 'https://deno.land/std@0.203.0/fs/ensure_file.ts'\nimport { Select } from 'cliffy'\n\ntype VSCodeSettings = {\n  files: string[]\n  vscode: Record<string, unknown>\n  imports: string[]\n}\n\nexport async function newCommand() {\n  const platform = await Select.prompt({\n    message: 'Where do you plan to deploy your app?',\n    options: [\n      { name: 'Deno 🦕', value: 'deno' },\n      { name: 'Cloudflare Workers ⚡', value: 'cloudflare' },\n    ],\n  })\n\n  let url =\n    `https://raw.githubusercontent.com/boywithkeyboard/templates/dev/${platform}/`\n\n  url += 'starter/'\n\n  const meta: VSCodeSettings = JSON.parse(\n    await (await fetch(url + 'meta.json')).text(),\n  )\n\n  const importMap: Record<string, string> = JSON.parse(\n    await (await fetch(\n      'https://raw.githubusercontent.com/boywithkeyboard/templates/dev/deno.json',\n    )).text(),\n  ).imports\n\n  for (const f of meta.files) {\n    const res = await fetch(url + f)\n\n    await ensureFile(f)\n    await Deno.writeTextFile(f, await res.text())\n  }\n\n  const imports = {}\n\n  for (const i of meta.imports) {\n    // @ts-ignore:\n    imports[i] = importMap[i]\n  }\n\n  await ensureFile('.vscode/settings.json')\n  await Deno.writeTextFile('.vscode/settings.json', JSON.stringify(meta.vscode))\n\n  if (platform === 'cloudflare') {\n    await Deno.writeTextFile(\n      'wrangler.toml',\n      `name = \"<name>\"\naccount_id = \"<account_id>\"\nroute = \"<route>\"\ncompatibility_date = \"2023-03-24\"\n    \n[build]\ncommand = \"deno task build\"`,\n    )\n  }\n\n  const version =\n    (await (await fetch('https://apiland.deno.dev/v2/modules/cheetah')).json())\n      .latest_version\n\n  await Deno.writeTextFile(\n    'deno.json',\n    JSON.stringify(\n      {\n        fmt: {\n          semiColons: false,\n          singleQuote: true,\n        },\n        imports,\n        tasks: {\n          build: `deno run -A https://deno.land/x/cheetah@${version}/build.ts`,\n        },\n      },\n      null,\n      2,\n    ),\n  )\n\n  const command = new Deno.Command('deno', {\n    args: ['fmt', '--single-quote', '--no-semicolons'],\n    stdout: 'piped',\n  })\n\n  command.spawn()\n}\n"
  },
  {
    "path": "cli/cmd/random/create_crypto_key.ts",
    "content": "// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.\nimport { encode } from 'std/encoding/base64.ts'\n\nexport async function createCryptoKey() {\n  const key = await crypto.subtle.generateKey(\n    { name: 'AES-GCM', hash: 'SHA-512', length: 256 },\n    true,\n    ['encrypt', 'decrypt'],\n  )\n\n  const exportedKey = await crypto.subtle.exportKey('raw', key)\n\n  return encode(exportedKey)\n}\n"
  },
  {
    "path": "cli/cmd/random/create_jwt_secret.ts",
    "content": "// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.\nimport { encode } from 'std/encoding/base64.ts'\n\nexport async function createJwtSecret() {\n  const key = await crypto.subtle.generateKey(\n    { name: 'HMAC', hash: 'SHA-512' },\n    true,\n    ['sign', 'verify'],\n  )\n\n  const exportedKey = await crypto.subtle.exportKey('raw', key)\n\n  return encode(exportedKey)\n}\n"
  },
  {
    "path": "cli/cmd/random/mod.ts",
    "content": "// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.\nimport { Select } from 'cliffy'\nimport { gray, white } from 'std/fmt/colors.ts'\nimport { createCryptoKey } from './create_crypto_key.ts'\nimport { createJwtSecret } from './create_jwt_secret.ts'\n\nexport async function randomCommand() {\n  const type: string = await Select.prompt({\n    message: 'What do you want to create?',\n    options: [\n      { name: 'JWT Secret', value: 'jwt_secret' },\n      { name: 'Crypto Key', value: 'crypto_key' },\n    ],\n  })\n\n  if (type === 'jwt_secret') {\n    console.info(\n      gray(`🗝️ ${white(type.replace('_', ' '))} - ${await createJwtSecret()}`),\n    )\n  } else {\n    console.info(\n      gray(`🗝️ ${white(type.replace('_', ' '))} - ${await createCryptoKey()}`),\n    )\n  }\n}\n"
  },
  {
    "path": "cli/cmd/serve/mod.ts",
    "content": "// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.\nimport { keypress, KeyPressEvent } from 'cliffy/keypress'\nimport { loadSync } from 'std/dotenv/mod.ts'\nimport { parse } from 'std/flags/mod.ts'\nimport { brightGreen, gray, white } from 'std/fmt/colors.ts'\nimport { cheetah } from '../../../cheetah.ts'\nimport { logError } from '../../utils.ts'\n\nexport async function serveCommand(args: ReturnType<typeof parse>) {\n  if (typeof args._[1] !== 'string') {\n    return logError('please specify an entry point')\n  }\n\n  loadSync({ export: true })\n\n  Deno.env.set('DEV', 'true')\n\n  const exports = await import(args._[1] as string)\n\n  let childProcess: Deno.ChildProcess\n\n  if (exports.fetch || exports.default && exports.default instanceof cheetah) {\n    if (exports.fetch) {\n      Deno.writeTextFileSync(\n        './__app.js',\n        `const { fetch: f } = await import('${\n          args._[1]\n        }')\\nDeno.serve((r, d) => f(r, d))`,\n      )\n\n      const cmd = new Deno.Command('deno', {\n        args: ['run', '-A', '--watch', '__app.js'],\n      })\n\n      childProcess = cmd.spawn()\n    } else {\n      Deno.writeTextFileSync(\n        './__app.js',\n        `const { default: a } = await import('${\n          args._[1]\n        }')\\nDeno.serve((r, d) => a.fetch(r, d))`,\n      )\n\n      const cmd = new Deno.Command('deno', {\n        args: ['run', '-A', '--watch', '__app.js'],\n      })\n\n      childProcess = cmd.spawn()\n    }\n  } else {\n    const cmd = new Deno.Command('deno', {\n      args: ['run', '-A', '--watch', args._[1]],\n    })\n\n    childProcess = cmd.spawn()\n  }\n\n  console.info(\n    gray(`${brightGreen('success')} - press ${white('CTRL+C')} to exit`),\n  )\n\n  for await (const e of keypress()) {\n    const { ctrlKey, key } = e as KeyPressEvent\n\n    if (ctrlKey && key === 'c' || key === 'escape') {\n      try {\n        Deno.removeSync('./__app.js')\n      } catch (_err) {\n        //\n      }\n\n      childProcess.kill()\n\n      Deno.exit()\n    }\n  }\n}\n"
  },
  {
    "path": "cli/mod.ts",
    "content": "// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.\nimport { parse } from 'std/flags/mod.ts'\nimport { bundleCommand } from './cmd/bundle/mod.ts'\nimport { newCommand } from './cmd/new/mod.ts'\nimport { randomCommand } from './cmd/random/mod.ts'\nimport { serveCommand } from './cmd/serve/mod.ts'\nimport { logError } from './utils.ts'\n\nconst args = parse(Deno.args)\n\nif (args._[0] === 'bundle') {\n  bundleCommand(args)\n} else if (args._[0] === 'random') {\n  randomCommand()\n} else if (args._[0] === 'new') {\n  newCommand()\n} else if (args._[0] === 'serve') {\n  serveCommand(args)\n} else {\n  logError('unknown command')\n}\n"
  },
  {
    "path": "cli/utils.ts",
    "content": "// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.\nimport { brightRed, gray } from 'std/fmt/colors.ts'\n\nexport function logError(message: string) {\n  console.error(gray(`${brightRed('error')} - ${message}`))\n}\n"
  },
  {
    "path": "codeowners",
    "content": "* @boywithkeyboard\n"
  },
  {
    "path": "collection.ts",
    "content": "// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.\nimport { base, Method } from './base.ts'\nimport { HandlerOrSchema } from './handler.ts'\n\nexport class Collection extends base<Collection>() {\n  #cors:\n    | string\n    | undefined\n\n  routes: Set<[\n    Uppercase<Method>,\n    string,\n    HandlerOrSchema[],\n  ]>\n\n  constructor({\n    cors,\n  }: {\n    /**\n     * Enable Cross-Origin Resource Sharing (CORS) for this collection by setting a origin, e.g. `*`.\n     *\n     * @since v0.11\n     */\n    cors?: string\n  } = {}) {\n    super((method, pathname, handlers) => {\n      if (this.#cors && typeof handlers[0] !== 'function') {\n        if (!handlers[0].cors) {\n          handlers[0].cors = this.#cors\n        }\n      }\n\n      this.routes.add([method, pathname, handlers])\n\n      return this\n    })\n\n    this.#cors = cors\n    this.routes = new Set()\n  }\n}\n"
  },
  {
    "path": "context.ts",
    "content": "// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.\n/// <reference types='./env.d.ts' />\nimport { encode } from 'std/encoding/base64.ts'\nimport { ZodType } from 'zod'\nimport { AppContext } from './cheetah.ts'\nimport { ObjectType, Payload } from './handler.ts'\nimport { RequestContext } from './request_context.ts'\nimport { ResponseContext } from './response_context.ts'\n\nconst HTTP_MESSAGES = {\n  'Bad Request': 400,\n  'Unauthorized': 401,\n  'Access Denied': 403,\n  'Not Found': 404,\n  'Method Not Allowed': 405,\n  'Not Acceptable': 406,\n  'Request Timeout': 408,\n  'Conflict': 409,\n  'Gone': 410,\n  'Length Required': 411,\n  'Precondition Failed': 412,\n  'Content Too Large': 413,\n  'URI Too Long': 414,\n  'Unsupported Media Type': 415,\n  'Range Not Satisfiable': 416,\n  'Expectation Failed': 417,\n  'Teapot': 418,\n  'Misdirected': 421,\n  'Upgrade Required': 426,\n  'Precondition Required': 428,\n  'Rate Limit Exceeded': 429,\n  'Regional Ban': 451,\n  'Something Went Wrong': 500,\n}\n\nexport class Context<\n  Params extends Record<string, unknown> = Record<string, unknown>,\n  ValidatedBody extends ZodType = never,\n  ValidatedCookies extends ObjectType = never,\n  ValidatedHeaders extends ObjectType = never,\n  ValidatedQuery extends ObjectType = never,\n> {\n  #a\n  #c: Cache | undefined\n  #i\n  #p\n  #r\n  #s\n  #req:\n    | RequestContext<\n      Params,\n      ValidatedBody,\n      ValidatedCookies,\n      ValidatedHeaders,\n      ValidatedQuery\n    >\n    | undefined\n  #res: ResponseContext | undefined\n  /**\n   * Wait until the response is sent to the client, then resolve the promise.\n   */\n  waitUntil: (promise: Promise<unknown>) => void\n\n  constructor(\n    __app: AppContext,\n    __internal: {\n      b: Exclude<Payload, void> | null\n      c: number\n      h: Headers\n    },\n    p: Record<string, string | undefined>,\n    r: Request,\n    s: {\n      body?: ZodType | undefined\n      cookies?: ObjectType | undefined\n      headers?: ObjectType | undefined\n      query?: ObjectType | undefined\n      [key: string]: unknown\n    } | null,\n    waitUntil: (promise: Promise<unknown>) => void,\n  ) {\n    this.#a = __app\n    this.#i = __internal\n    this.#p = p\n    this.#r = r\n    this.#s = s\n    this.waitUntil = waitUntil\n  }\n\n  get __app(): AppContext {\n    return this.#a\n  }\n\n  get cache(): Cache {\n    if (this.#c) {\n      return this.#c\n    }\n\n    this.#c = new Cache(this)\n\n    return this.#c\n  }\n\n  get dev(): boolean {\n    return this.#a.debugging ||\n      this.runtime === 'deno' && Deno.env.get('DEV') === 'true'\n  }\n\n  env<T extends keyof Variables>(name: T): Variables[T] {\n    return this.runtime === 'deno'\n      ? Deno.env.get(name)\n      : (this.#a.env as Variables)[name]\n  }\n\n  get req(): RequestContext<\n    Params,\n    ValidatedBody,\n    ValidatedCookies,\n    ValidatedHeaders,\n    ValidatedQuery\n  > {\n    if (this.#req) {\n      return this.#req\n    }\n\n    this.#req = new RequestContext(\n      this.#a,\n      this.#p,\n      this.#r,\n      this.#s,\n      this.exception,\n    )\n\n    return this.#req\n  }\n\n  get res(): ResponseContext {\n    if (this.#res) {\n      return this.#res\n    }\n\n    this.#res = new ResponseContext(this.#i)\n\n    return this.#res\n  }\n\n  get runtime() {\n    return this.#a.runtime\n  }\n\n  exception(error: keyof typeof HTTP_MESSAGES, description?: string) {\n    const code = HTTP_MESSAGES[error]\n\n    return new Exception(error, description, code)\n  }\n}\n\n/** @private */\nexport class Exception {\n  public response\n\n  constructor(\n    error: string,\n    description: string | undefined,\n    code: number,\n  ) {\n    this.response = (request: Request) => {\n      const a = request.headers.get('accept')\n\n      const json = a\n        ? a.indexOf('application/json') > -1 ||\n          a.indexOf('*/*') > -1\n        : false\n\n      return new Response(\n        json ? JSON.stringify({ error, description, code }) : error,\n        {\n          headers: {\n            'content-type': `${\n              json ? 'application/json' : 'text/plain'\n            }; charset=utf-8;`,\n          },\n          status: code,\n        },\n      )\n    }\n  }\n}\n\nclass Cache {\n  #cache: globalThis.Cache | null\n  #context\n  #name\n\n  constructor(c: Context) {\n    this.#cache = null\n    this.#context = c\n    this.#name = c.__app.caching?.name ?? 'cheetah'\n  }\n\n  async set(\n    key: string,\n    value: string | Record<string, unknown> | Uint8Array,\n    options?: {\n      maxAge?: number\n    },\n  ) {\n    if (this.#cache === null) {\n      this.#cache = await caches.open(this.#name)\n    }\n\n    this.#context.waitUntil(\n      this.#cache.put(\n        `https://${this.#name}.com/${encode(key)}`,\n        new Response(\n          typeof value === 'string' || value instanceof Uint8Array\n            ? value\n            : JSON.stringify(value),\n          {\n            headers: {\n              'cache-control': `max-age=${options?.maxAge ?? 300}`,\n            },\n          },\n        ),\n      ),\n    )\n  }\n\n  get<T extends Record<string, unknown> = Record<string, unknown>>(\n    key: string,\n    type: 'json',\n  ): Promise<T | undefined>\n  get<T extends string = string>(\n    key: string,\n    type: 'string',\n  ): Promise<T | undefined>\n  get<T extends Uint8Array = Uint8Array>(\n    key: string,\n    type: 'buffer',\n  ): Promise<T | undefined>\n\n  async get<T extends Record<string, unknown> | string | Uint8Array>(\n    key: string,\n    type: 'string' | 'json' | 'buffer' = 'string',\n  ): Promise<T | undefined> {\n    if (this.#cache === null) {\n      this.#cache = await caches.open(this.#name)\n    }\n\n    try {\n      const result = await this.#cache.match(\n        `https://${this.#name}.com/${encode(key)}`,\n      )\n\n      if (!result) {\n        return undefined\n      }\n\n      const data = type === 'string'\n        ? await result.text()\n        : type === 'json'\n        ? await result.json()\n        : new Uint8Array(await result.arrayBuffer())\n\n      return data\n    } catch (_err) {\n      return undefined\n    }\n  }\n\n  async has(key: string) {\n    if (this.#cache === null) {\n      this.#cache = await caches.open(this.#name)\n    }\n\n    const result = await this.#cache.match(\n      `https://${this.#name}.com/${encode(key)}`,\n    )\n\n    return result !== undefined\n  }\n\n  async delete(key: string) {\n    if (this.#cache === null) {\n      this.#cache = await caches.open(this.#name)\n    }\n\n    this.#context.waitUntil(\n      this.#cache.delete(\n        `https://${this.#name}.com/${encode(key)}`,\n      ),\n    )\n  }\n}\n"
  },
  {
    "path": "contributing.md",
    "content": "### Follow Conventional Commits\n\nThis repository enforces the\n[Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/)\nspecification for writing pull requests and issues titles.\n\n### Write a meaningful title\n\nWe ask you to write a short and meaningful title while going into more detail\nabout your changes in the description of the pull request.\n\nTypically, you should receive a response to your pull request within a few\nhours.\n\n### Make sure nothing's broken\n\nAfter you've made your changes, please run `deno task check` to format & check\nyour code and run the test suites.\n"
  },
  {
    "path": "crypto.ts",
    "content": "// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.\nimport { decode } from 'std/encoding/base64.ts'\nimport { Context } from './context.ts'\n\nexport async function encrypt(c: Context, message: string) {\n  const key = (c.env('crypto_key') ?? c.env('CRYPTO_KEY')) as string\n\n  const iv = crypto.getRandomValues(new Uint8Array(12))\n  const ivStr = Array.from(iv)\n    .map((byte) => String.fromCharCode(byte))\n    .join('')\n  const alg = { name: 'AES-GCM', iv }\n  const cryptoKey = await crypto.subtle.importKey(\n    'raw',\n    decode(key).buffer,\n    alg,\n    true,\n    ['encrypt', 'decrypt'],\n  )\n  const cipherBuf = await crypto.subtle.encrypt(\n    alg,\n    cryptoKey,\n    new TextEncoder().encode(message),\n  )\n  const cipherArr = Array.from(new Uint8Array(cipherBuf))\n  const cipherStr = cipherArr.map((byte) => String.fromCharCode(byte))\n    .join('')\n\n  return btoa(ivStr + cipherStr)\n}\n\nexport async function decrypt(c: Context, message: string) {\n  const key = (c.env('crypto_key') ?? c.env('CRYPTO_KEY')) as string\n\n  const iv = atob(message).slice(0, 12)\n  const alg = {\n    name: 'AES-GCM',\n    iv: new Uint8Array(\n      Array.from(iv).map((char) => char.charCodeAt(0)),\n    ),\n  }\n  const cryptoKey = await crypto.subtle.importKey(\n    'raw',\n    decode(key).buffer,\n    alg,\n    true,\n    ['encrypt', 'decrypt'],\n  )\n  const cipherStr = atob(message).slice(12)\n  const cipherBuf = new Uint8Array(\n    Array.from(cipherStr).map((char) => char.charCodeAt(0)),\n  )\n  const buf = await crypto.subtle.decrypt(alg, cryptoKey, cipherBuf)\n\n  return new TextDecoder().decode(buf)\n}\n"
  },
  {
    "path": "deno.json",
    "content": "{\n  \"compilerOptions\": {\n    \"jsxFactory\": \"h\",\n    \"jsxFragmentFactory\": \"Fragment\"\n  },\n  \"fmt\": {\n    \"semiColons\": false,\n    \"singleQuote\": true,\n    \"exclude\": [\n      \"./changelog.md\",\n      \"./readme.md\"\n    ]\n  },\n  \"imports\": {\n    \"authenticus\": \"https://deno.land/x/authenticus@v2.0.3/mod.ts\",\n    \"authenticus/preset\": \"https://deno.land/x/authenticus@v2.0.3/createPreset.ts\",\n    \"aws4fetch\": \"https://esm.sh/aws4fetch@1.0.17?target=es2022\",\n    \"brotli\": \"https://deno.land/x/brotli@0.1.7/mod.ts\",\n    \"cliffy\": \"https://deno.land/x/cliffy@v0.25.7/mod.ts\",\n    \"cliffy/keypress\": \"https://deno.land/x/cliffy@v0.25.7/keypress/mod.ts\",\n    \"djwt\": \"https://deno.land/x/djwt@v2.8/mod.ts\",\n    \"dom\": \"https://deno.land/x/deno_dom@v0.1.40/deno-dom-wasm.ts\",\n    \"esbuild\": \"https://deno.land/x/esbuild@v0.19.3/mod.js\",\n    \"foras\": \"https://deno.land/x/foras@v2.1.1/src/deno/mod.ts\",\n    \"otpauth\": \"https://deno.land/x/otpauth@v9.1.4/dist/otpauth.esm.js\",\n    \"preact\": \"https://esm.sh/preact@10.17.1?target=es2022\",\n    \"preact/render-to-string\": \"https://esm.sh/preact-render-to-string@6.1.0?deps=preact@10.17.1&target=es2022\",\n    \"std/\": \"https://deno.land/std@0.203.0/\",\n    \"twind\": \"https://esm.sh/@twind/core@1.1.3\",\n    \"twind/preset-autoprefix\": \"https://esm.sh/@twind/preset-autoprefix@1.0.7\",\n    \"twind/preset-tailwind\": \"https://esm.sh/@twind/preset-tailwind@1.1.4\",\n    \"worker\": \"https://cdn.jsdelivr.net/npm/@cloudflare/workers-types@4.20230922.0/index.ts\",\n    \"upstash\": \"https://deno.land/x/upstash_redis@v1.22.0/mod.ts\",\n    \"zod\": \"https://deno.land/x/zod@v3.21.4/mod.ts\"\n  },\n  \"lock\": false,\n  \"tasks\": {\n    \"check\": \"deno fmt && deno lint --unstable && deno task test\",\n    \"test\": \"deno test -A --unstable\"\n  }\n}\n"
  },
  {
    "path": "env.d.ts",
    "content": "declare global {\n  type Variables = Record<string, unknown>\n}\n\nexport {}\n"
  },
  {
    "path": "ext/compress.ts",
    "content": "// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.\nimport { compress as brotli } from 'brotli'\nimport { deflate, gzip, initBundledOnce, InitOutput } from 'foras'\nimport { createExtension } from '../mod.ts'\n\ntype CompressionAlgorithm = {\n  format: 'br' | 'deflate' | 'gzip'\n  compress: (input: Uint8Array) => Uint8Array | Promise<Uint8Array>\n}\n\nlet FORAS: InitOutput | undefined\n\nexport const BROTLI: CompressionAlgorithm = {\n  format: 'br',\n  compress: brotli,\n}\n\nexport const DEFLATE: CompressionAlgorithm = {\n  format: 'deflate',\n  async compress(input) {\n    if (!FORAS) {\n      FORAS = await initBundledOnce()\n    }\n\n    return deflate(input)\n  },\n}\n\nexport const GZIP: CompressionAlgorithm = {\n  format: 'gzip',\n  async compress(input) {\n    if (!FORAS) {\n      FORAS = await initBundledOnce()\n    }\n\n    return gzip(input)\n  },\n}\n\n/**\n * 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.\n *\n * @since v1.0\n */\nexport const compress = createExtension<{\n  algorithm: CompressionAlgorithm | CompressionAlgorithm[]\n}>({\n  async onResponse({ c, _ }) {\n    const alg = _.algorithm instanceof Array ? _.algorithm : [_.algorithm]\n\n    const header = c.req.headers['accept-encoding']\n\n    if (c.res.body === null || !header || header === 'identity') {\n      return\n    }\n\n    for (let i = 0; i < alg.length; ++i) {\n      if (!header.includes(alg[i].format)) {\n        continue\n      }\n\n      c.res.body = c.res.body instanceof Uint8Array\n        ? await alg[i].compress(c.res.body)\n        : c.res.body instanceof ArrayBuffer\n        ? await alg[i].compress(new Uint8Array(c.res.body))\n        : typeof c.res.body === 'string'\n        ? await alg[i].compress(new TextEncoder().encode(c.res.body))\n        : c.res.body\n\n      if (c.res.body instanceof Uint8Array) {\n        c.res.header('content-encoding', alg[i].format)\n      }\n\n      break\n    }\n  },\n})\n"
  },
  {
    "path": "ext/debug.ts",
    "content": "// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.\nimport { format } from 'std/fmt/bytes.ts'\nimport {\n  brightBlue,\n  brightGreen,\n  brightRed,\n  gray,\n  white,\n} from 'std/fmt/colors.ts'\nimport { createExtension } from '../mod.ts'\n\n/**\n * An extension to log every response (that didn't throw an exception).\n *\n * @since v1.0\n */\nexport const debug = createExtension({\n  onResponse({ c }) {\n    const code = c.res.code.toString()\n\n    console.info(\n      gray(\n        `${\n          code.startsWith('3')\n            ? brightBlue(c.res.code.toString())\n            : code.startsWith('2')\n            ? brightGreen(c.res.code.toString())\n            : brightRed(c.res.code.toString())\n        } - ${c.req.method} ${white(new URL(c.req.raw.url).pathname)}${\n          c.res.bodySize > -1 ? ` (${format(c.res.bodySize)})` : ''\n        }`,\n      ),\n    )\n  },\n})\n"
  },
  {
    "path": "ext/favicon.ts",
    "content": "// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.\nimport { createExtension } from '../mod.ts'\n\nlet FAVICON: ArrayBuffer | Uint8Array | undefined\n\n/**\n * An extension to set a neat [favicon](https://en.wikipedia.org/wiki/Favicon) for your app.\n *\n * The source for your favicon can be either a URL or a buffer.\n *\n * @since v1.1\n */\nexport const favicon = createExtension<{\n  headers?: Record<string, string>\n  source: string | ArrayBuffer | Uint8Array\n}>({\n  async onRequest({\n    req,\n    _: {\n      headers,\n      source,\n    },\n  }) {\n    const parts = req.url.split('?')\n\n    parts[0] = parts[0].slice(8)\n\n    const pathname = parts[0].substring(parts[0].indexOf('/'))\n\n    if (pathname !== '/favicon.ico') {\n      return\n    }\n\n    if (!FAVICON) {\n      if (typeof source === 'string') {\n        const response = await fetch(source)\n\n        FAVICON = await response.arrayBuffer()\n      } else {\n        FAVICON = source\n      }\n    }\n\n    return new Response(FAVICON, {\n      headers,\n    })\n  },\n})\n"
  },
  {
    "path": "ext/files.ts",
    "content": "// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.\nimport { AwsClient } from 'aws4fetch'\nimport { join } from 'std/path/mod.ts'\nimport { R2Bucket } from 'worker'\nimport { createExtension } from '../extensions.ts'\nimport { AppContext } from '../mod.ts'\n\ntype GeneralOptions = {\n  cacheControl?: string\n  etag?: boolean\n}\n\ntype FsOptions = {\n  type?: 'fs'\n  directory: string\n}\n\ntype R2Options = {\n  type: 'r2'\n  name: string\n}\n\ntype S3Options = {\n  type: 's3'\n  endpoint?: string\n  accessKeyId?: string\n  secretAccessKey?: string\n}\n\nconst awsClient = new AwsClient({\n  accessKeyId: '',\n  secretAccessKey: '',\n})\n\nfunction getVar<T extends unknown = string | undefined>(\n  app: AppContext,\n  name: string,\n): T {\n  return app.runtime === 'cloudflare' && app.env\n    ? app.env[name] as T\n    : Deno.env.get(name) as T\n}\n\n/**\n * An extension to serve static files from Cloudflare R2, an S3 bucket, or the local file system.\n *\n * @copyright [@not-ivy](https://github.com/not-ivy), [@boywithkeyboard](https://github.com/boywithkeyboard)\n * @since v1.2\n */\nexport const files = createExtension<{\n  serve: GeneralOptions & (FsOptions | R2Options | S3Options)\n}>({\n  // onPlugIn({ settings }) {\n  //     if (settings.serve.type === 's3') {\n  //     awsClient = new AwsClient({\n  //       accessKeyId: settings.serve.accessKeyId,\n  //       secretAccessKey: settings.serve.secretAccessKey,\n  //     })\n  //   }\n  // },\n  onRequest({\n    app,\n    prefix,\n    _: {\n      serve,\n    },\n    req: request,\n  }) {\n    switch (serve.type) {\n      case 'r2':\n        return handleR2Files(app, serve, prefix)\n      case 's3': {\n        const keyId = getVar(app, 'S3_ACCESS_KEY_ID') ??\n          getVar(app, 's3_access_key_id') ?? serve.accessKeyId\n        if (!keyId) throw new Error('S3_ACCESS_KEY_ID is not set')\n        const accessKey = getVar(app, 'S3_SECRET_ACCESS_KEY') ??\n          getVar(app, 's3_secret_access_key') ??\n          serve.secretAccessKey\n        if (!accessKey) throw new Error('S3_SECRET_ACCESS_KEY is not set')\n        awsClient.accessKeyId = keyId\n        awsClient.secretAccessKey = accessKey\n        return handleS3Files(app, serve, prefix, request)\n      }\n      case 'fs':\n      default:\n        return handleFsFiles(app, serve, prefix)\n    }\n  },\n})\n\nasync function handleS3Files(\n  app: AppContext,\n  serve: GeneralOptions & S3Options,\n  prefix: string,\n  request: Request,\n) {\n  const path = join(\n    prefix !== '*'\n      ? app.request.pathname.substring(prefix.length + 1)\n      : app.request.pathname,\n  )\n\n  let response = await awsClient.fetch(\n    `${serve.endpoint}${path}`,\n    {\n      headers: {\n        ...(serve.etag !== false &&\n          { etag: request.headers.get('if-none-match') ?? '' }),\n        'cache-control': serve.cacheControl ?? 's-maxage=300', // 5m\n      },\n    },\n  )\n\n  if (response.status === 404) {\n    const indexPath = join(path, 'index.html')\n    response = await awsClient.fetch(\n      `${serve.endpoint}${indexPath}`,\n      {\n        headers: {\n          ...(serve.etag !== false &&\n            { etag: request.headers.get('if-none-match') ?? '' }),\n          'cache-control': serve.cacheControl ?? 's-maxage=300', // 5m\n        },\n      },\n    )\n    if (response.status === 404) {\n      const indexPath = join(path, '404.html')\n      response = await awsClient.fetch(\n        `${serve.endpoint}${indexPath}`,\n        {\n          headers: {\n            'cache-control': serve.cacheControl ?? 's-maxage=300', // 5m\n          },\n        },\n      )\n    }\n  }\n\n  return response.status === 404 ? undefined : response\n}\n\nasync function handleR2Files(\n  app: AppContext,\n  serve: GeneralOptions & R2Options,\n  prefix: string,\n) {\n  if (app.runtime !== 'cloudflare' || !app.env) {\n    throw new Error(\n      'You need to use the Cloudflare Workers runtime to serve static files from an R2 bucket!',\n    )\n  }\n\n  const bucket = app.env[serve.name] as R2Bucket\n  const path = prefix !== '*'\n    ? app.request.pathname.substring(prefix.length + 1)\n    : app.request.pathname\n\n  let object = await bucket.get(path)\n  if (object) {\n    return new Response(object.body as ReadableStream, {\n      headers: {\n        ...(serve.etag !== false && { etag: object.httpEtag }),\n        'cache-control': serve.cacheControl ?? 's-maxage=300', // 5m\n      },\n    })\n  }\n\n  const indexPath = join(app.request.pathname, 'index.html')\n  object = await bucket.get(indexPath)\n  if (object) {\n    return new Response(object.body as ReadableStream, {\n      headers: {\n        ...(serve.etag !== false && { etag: object.httpEtag }),\n        'cache-control': serve.cacheControl ?? 's-maxage=300', // 5m\n      },\n    })\n  }\n\n  const errorPath = join(prefix, '404.html')\n  object = await bucket.get(errorPath)\n  if (object) {\n    return new Response(object.body as ReadableStream, {\n      headers: {\n        'cache-control': serve.cacheControl ?? 's-maxage=300', // 5m\n      },\n    })\n  }\n}\n\nasync function handleFsFiles(\n  app: AppContext,\n  serve: GeneralOptions & FsOptions,\n  prefix: string,\n) {\n  const path = join(\n    serve.directory,\n    prefix !== '*'\n      ? app.request.pathname.substring(prefix.length + 1)\n      : app.request.pathname,\n  )\n\n  let stat: Deno.FileInfo\n  let file: Deno.FsFile\n\n  try {\n    stat = await Deno.lstat(path)\n    if (stat.isDirectory) {\n      stat = await Deno.lstat(join(path, 'index.html'))\n      file = await Deno.open(join(path, 'index.html'), { read: true })\n    } else {\n      file = await Deno.open(path, { read: true })\n    }\n  } catch {\n    try {\n      stat = await Deno.lstat(join(serve.directory, '404.html'))\n      file = await Deno.open(join(serve.directory, '404.html'), { read: true })\n    } catch {\n      return\n    }\n  }\n\n  return new Response(file.readable, {\n    headers: {\n      ...(serve.etag !== false && { etag: await etag(stat) }),\n      'cache-control': serve.cacheControl ?? 's-maxage=300', // 5m\n    },\n  })\n}\n\nasync function etag(stat: Deno.FileInfo) {\n  const encoder = new TextEncoder()\n  const data = encoder.encode(\n    `${stat.birthtime?.getTime()}:${stat.mtime?.getTime()}:${stat.size}`,\n  )\n  const hash = await crypto.subtle.digest({ name: 'SHA-1' }, data)\n  const hashArray = Array.from(new Uint8Array(hash))\n  const hashHex = hashArray.map((b) => b.toString(16).padStart(2, '0')).join('')\n  return hashHex\n}\n"
  },
  {
    "path": "ext/firewall.ts",
    "content": "import { createExtension } from '../mod.ts'\n\nlet VPN_LIST: string[] = []\nlet DATACENTER_LIST: string[] = []\nlet LAST_UPDATE = 0\n\ntype FirewallOptions = {\n  blockVPN?: boolean\n  blockDatacenter?: boolean\n  customRanges?: string[]\n}\n\n/**\n * An extension for blocking specific traffic to your app.\n *\n * @namespace ext\n * @since v1.4\n */\nexport const firewall = createExtension<FirewallOptions>({\n  async onRequest({ _: opts, app }) {\n    await checkUpdate()\n    if (\n      opts?.blockVPN && VPN_LIST.find((range) => isIpInRange(app.ip, range))\n    ) {\n      return new Response(null, { status: 403 })\n    }\n    if (\n      opts?.blockDatacenter &&\n      DATACENTER_LIST.find((range) => isIpInRange(app.ip, range))\n    ) return new Response(null, { status: 403 })\n    if (\n      opts?.customRanges &&\n      opts.customRanges.find((range) => isIpInRange(app.ip, range))\n    ) return new Response(null, { status: 403 })\n  },\n})\n\nasync function checkUpdate() {\n  if (Date.now() - LAST_UPDATE < 9e5 /* 15 minutes */) return\n  VPN_LIST = (await (await fetch(\n    'https://raw.githubusercontent.com/X4BNet/lists_vpn/main/output/vpn/ipv4.txt',\n  )).text()).split('\\n')\n  DATACENTER_LIST = (await (await fetch(\n    'https://raw.githubusercontent.com/X4BNet/lists_vpn/main/output/datacenter/ipv4.txt',\n  )).text()).split('\\n')\n  LAST_UPDATE = Date.now()\n}\n\nfunction isIpInRange(ip: string, range: string) {\n  const [rangeIp, rangeMask] = range.split('/')\n  const rangeStart = ipToNumber(rangeIp) >>> 0\n  const rangeEnd = rangeStart + ((1 << (32 - parseInt(rangeMask))) - 1)\n  const numIp = ipToNumber(ip)\n  return numIp >= rangeStart && numIp <= rangeEnd\n}\n\nfunction ipToNumber(ip: string) {\n  return ip.split('.').reduce((acc, val) => (acc << 8) | parseInt(val), 0) >>> 0\n}\n"
  },
  {
    "path": "ext/helmet.ts",
    "content": "// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.\nimport { createExtension } from '../mod.ts'\n\n/**\n * An extension to attach important security headers to the response.\n *\n * @since v1.0\n */\nexport const helmet = createExtension<{\n  /**\n   * Set the `Content-Security-Policy` header with a strict security policy.\n   *\n   * @default\n   * true\n   */\n  contentSecurityPolicy?: boolean\n\n  /**\n   * Set the `Cross-Origin-Embedder-Policy` header.\n   *\n   * @default\n   * null\n   */\n  crossOriginEmbedderPolicy?:\n    | 'require-corp'\n    | 'unsafe-none'\n    | 'credentialless'\n    | null\n\n  /**\n   * Set the `Cross-Origin-Opener-Policy` header.\n   *\n   * @default\n   * 'same-origin'\n   */\n  crossOriginOpenerPolicy?:\n    | 'same-origin'\n    | 'same-origin-allow-popups'\n    | 'unsafe-none'\n    | null\n\n  /**\n   * Set the `Cross-Origin-Resource-Policy` header.\n   *\n   * @default\n   * 'same-origin'\n   */\n  crossOriginResourcePolicy?:\n    | 'same-origin'\n    | 'same-site'\n    | 'cross-origin'\n    | null\n\n  /**\n   * Enable [DNS Prefetching](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-DNS-Prefetch-Control) at the expense of your users' privacy.\n   *\n   * @default\n   * false\n   */\n  dnsPrefetching?: boolean\n\n  /**\n   * Set the `X-Frame-Options` header to mitigate Clickjacking.\n   *\n   * @default\n   * 'sameorigin'\n   */\n  noFraming?:\n    | 'deny'\n    | 'sameorigin'\n    | null\n\n  /**\n   * Set the `Strict-Transport-Security` header, which indicates to browsers to prefer a secure HTTPS connection.\n   *\n   * @default\n   * {\n   *   maxAge: 31536000, // a year\n   *   includeSubDomains: true\n   * }\n   */\n  hsts?: {\n    maxAge?: number\n    preload?: boolean\n    includeSubDomains?: boolean\n  } | null\n\n  /**\n   * Set the `X-Content-Type-Options` header to `nosniff`. This mitigates Content Sniffing, which can cause security vulnerabilities.\n   *\n   * @default\n   * true\n   */\n  noSniffing?: boolean\n\n  /**\n   * Set the `Origin-Agent-Cluster` header, which provides a mechanism to allow web applications to isolate their origins.\n   *\n   * @default\n   * true\n   */\n  originAgentCluster?: boolean\n\n  /**\n   * Set the `X-Permitted-Cross-Domain-Policies` header, which tells some clients (mostly Adobe products) your domain's policy for loading cross-domain content.\n   *\n   * @default\n   * 'none'\n   */\n  crossDomainPolicy?:\n    | 'none'\n    | 'master-only'\n    | 'by-content-type'\n    | 'all'\n    | null\n\n  /**\n   * Set the `Referrer-Policy` header to control what information is set in the `Referer` header.\n   *\n   * @default\n   * 'no-referrer'\n   */\n  referrerPolicy?:\n    | 'no-referrer'\n    | 'no-referrer-when-downgrade'\n    | 'origin'\n    | 'origin-when-cross-origin'\n    | 'same-origin'\n    | 'strict-origin'\n    | 'strict-origin-when-cross-origin'\n    | 'unsafe-url'\n    | null\n}>({\n  onResponse({\n    c,\n    _: {\n      contentSecurityPolicy = true,\n      crossOriginEmbedderPolicy = null,\n      crossOriginOpenerPolicy = 'same-origin',\n      crossOriginResourcePolicy = 'same-origin',\n      dnsPrefetching = false,\n      noFraming = 'sameorigin',\n      hsts = {\n        maxAge: 31536000, // a year\n        includeSubDomains: true,\n      },\n      noSniffing = true,\n      originAgentCluster = true,\n      crossDomainPolicy = 'none',\n      referrerPolicy = 'no-referrer',\n    } = {},\n  }) {\n    if (contentSecurityPolicy) {\n      c.res.header(\n        'content-security-policy',\n        `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`,\n      )\n    }\n\n    if (crossOriginEmbedderPolicy !== null) {\n      c.res.header('cross-origin-embedder-policy', crossOriginEmbedderPolicy)\n    }\n\n    if (crossOriginOpenerPolicy !== null) {\n      c.res.header('cross-origin-opener-policy', crossOriginOpenerPolicy)\n    }\n\n    if (crossOriginResourcePolicy !== null) {\n      c.res.header('cross-origin-resource-policy', crossOriginResourcePolicy)\n    }\n\n    if (dnsPrefetching !== null) {\n      c.res.header('x-dns-prefetch-control', dnsPrefetching ? 'on' : 'off')\n    }\n\n    if (noFraming !== null) {\n      c.res.header('x-frame-options', noFraming.toUpperCase())\n    }\n\n    if (hsts !== null) {\n      c.res.header(\n        'strict-transport-security',\n        Object.entries(hsts)\n          .map(([key, value]) =>\n            `${key.replace('max-age', 'maxAge')}${\n              typeof value === 'boolean' ? '' : `=${value}`\n            }`\n          )\n          .join('; '),\n      )\n    }\n\n    if (noSniffing) {\n      c.res.header('x-content-type-options', 'nosniff')\n    }\n\n    if (originAgentCluster) {\n      c.res.header('origin-agent-cluster', '?1')\n    }\n\n    if (crossDomainPolicy !== null) {\n      c.res.header('x-permitted-cross-domain-policies', crossDomainPolicy)\n    }\n\n    if (referrerPolicy) {\n      c.res.header('referrer-policy', 'no-referrer')\n    }\n  },\n})\n"
  },
  {
    "path": "ext/pretty.ts",
    "content": "// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.\nimport { createExtension } from '../mod.ts'\n\nfunction sortObject(object: unknown) {\n  if (typeof object != 'object' || object instanceof Array || object === null) {\n    return object\n  }\n\n  const keys = Object.keys(object).sort()\n\n  const newObject: Record<string, unknown> = {}\n\n  for (let i = 0; i < keys.length; i++) {\n    newObject[keys[i]] = sortObject(\n      (object as Record<string, unknown>)[keys[i]],\n    )\n  }\n\n  return newObject\n}\n\n/**\n * An extension to jazz up the response body by adding indentation and sorting the keys alphabetically.\n *\n * @since v1.1\n */\nexport const pretty = createExtension<{\n  /**\n   * Apply indentation to your response body. Set it to `0` to disable indentation.\n   *\n   * @default 2\n   */\n  indentSize?: number\n  /**\n   * Whether to sort the fields alphabetically.\n   *\n   * @default false\n   */\n  sort?: boolean\n}>({\n  onResponse({\n    c,\n    _: {\n      indentSize = 2,\n      sort = true,\n    } = {},\n  }) {\n    let body = c.res.body\n\n    if (body === null || body.constructor.name !== 'Object') {\n      return\n    }\n\n    c.res.header('content-type', 'application/json; charset=utf-8')\n\n    if (sort) {\n      body = sortObject(body) as Record<string, unknown>\n    }\n\n    c.res.body = JSON.stringify(\n      body,\n      null,\n      indentSize,\n    )\n  },\n})\n"
  },
  {
    "path": "extensions.ts",
    "content": "// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.\nimport { ZodString, ZodUnion } from 'zod'\nimport { Method } from './base.ts'\nimport { Handler, ObjectType } from './handler.ts'\nimport { AppContext, Context } from './mod.ts'\n\ntype HasRequired<T> = Partial<T> extends T ? false : true\n\ntype Req = Response | void | undefined\ntype Res = void | undefined\n\ntype ExtensionContext = {\n  /** The prefix of the routes to which this extension is assigned. */\n  prefix: string\n}\n\nexport type Extension<\n  Config extends Record<string, unknown> | unknown = never,\n> = {\n  __config: Config | undefined\n  onPlugIn: HasRequired<Config> extends true ? ((\n      context: ExtensionContext & {\n        env: AppContext['env']\n        routes: AppContext['routes']\n        runtime: AppContext['runtime']\n        setRoute: <\n          Pathname extends `/${string}`,\n          // deno-lint-ignore no-explicit-any\n          ValidatedBody extends ObjectType | ZodString | ZodUnion<any>,\n          ValidatedCookies extends ObjectType,\n          ValidatedHeaders extends ObjectType,\n          ValidatedQuery extends ObjectType,\n        >(\n          method: Method,\n          pathname: Pathname,\n          ...handler: (\n            | {\n              body?: ValidatedBody\n              cookies?: ValidatedCookies\n              headers?: ValidatedHeaders\n              query?: ValidatedQuery\n              cors?: string\n            }\n            | Handler<\n              Pathname,\n              ValidatedBody,\n              ValidatedCookies,\n              ValidatedHeaders,\n              ValidatedQuery\n            >\n          )[]\n        ) => void | Promise<void>\n        settings: Config\n      },\n    ) => void | Promise<void>)\n    : ((\n      context: ExtensionContext & {\n        env: AppContext['env']\n        routes: AppContext['routes']\n        runtime: AppContext['runtime']\n        setRoute: <\n          Pathname extends `/${string}`,\n          // deno-lint-ignore no-explicit-any\n          ValidatedBody extends ObjectType | ZodString | ZodUnion<any>,\n          ValidatedCookies extends ObjectType,\n          ValidatedHeaders extends ObjectType,\n          ValidatedQuery extends ObjectType,\n        >(\n          method: Method,\n          pathname: Pathname,\n          ...handlers: (\n            | {\n              body?: ValidatedBody\n              cookies?: ValidatedCookies\n              headers?: ValidatedHeaders\n              query?: ValidatedQuery\n              cors?: string\n            }\n            | Handler<\n              Pathname,\n              ValidatedBody,\n              ValidatedCookies,\n              ValidatedHeaders,\n              ValidatedQuery\n            >\n          )[]\n        ) => void\n        settings?: Config\n      },\n    ) => void | Promise<void>)\n  onRequest?: HasRequired<Config> extends true ? ((\n      context: ExtensionContext & {\n        app: AppContext\n        req: Request\n        _: Config\n      },\n    ) => Req | Promise<Req>)\n    : ((\n      context: ExtensionContext & {\n        app: AppContext\n        req: Request\n        _?: Config\n      },\n    ) => Req | Promise<Req>)\n  onResponse?: HasRequired<Config> extends true ? ((\n      context: ExtensionContext & {\n        app: AppContext\n        c: Context\n        _: Config\n      },\n    ) => Res | Promise<Res>)\n    : ((\n      context: ExtensionContext & {\n        app: AppContext\n        c: Context\n        _?: Config\n      },\n    ) => Res | Promise<Res>)\n}\n\ntype ReturnFunction<\n  Config extends Record<string, unknown> | unknown = unknown,\n> = Config extends Record<string, unknown>\n  ? (HasRequired<Config> extends true ? ((config: Config) => Extension<Config>)\n    : ((config?: Config) => Extension<Config>))\n  : (() => Extension<Config>)\n\nexport function validExtension(ext: Record<string, unknown>) {\n  const symbol = Object.getOwnPropertySymbols(ext).find((s) =>\n    s.description === 'cheetah.extension'\n  )\n\n  // @ts-ignore:\n  return symbol !== undefined && ext[symbol] === 'v1.0'\n}\n\nexport function createExtension<\n  Config extends Record<string, unknown> | unknown = unknown,\n>({\n  onPlugIn,\n  onRequest,\n  onResponse,\n}: {\n  onPlugIn?: Extension<Config>['onPlugIn']\n  onRequest?: Extension<Config>['onRequest']\n  onResponse?: Extension<Config>['onResponse']\n}) {\n  return ((__config?: Config) => {\n    return {\n      __config,\n      onPlugIn,\n      onRequest,\n      onResponse,\n      [Symbol('cheetah.extension')]: 'v1.0',\n    }\n  }) as ReturnFunction<Config>\n}\n"
  },
  {
    "path": "handler.ts",
    "content": "// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.\nimport {\n  ZodObject,\n  ZodRecord,\n  ZodString,\n  ZodType,\n  ZodTypeDef,\n  ZodUnion,\n} from 'zod'\nimport { Context } from './mod.ts'\n\nexport type ObjectType =\n  // deno-lint-ignore no-explicit-any\n  | ZodObject<any>\n  | ZodRecord\n\nexport type BaseType<T extends ZodTypeDef = ZodTypeDef> =\n  // deno-lint-ignore no-explicit-any\n  ZodType<any, T, any>\n\ntype ExtractParam<Path, NextPart> = Path extends `:${infer Param}`\n  ? Record<Param, string> & NextPart\n  : NextPart\n\ntype ExtractParams<Path> = Path extends `${infer Segment}/${infer Rest}`\n  ? ExtractParam<Segment, ExtractParams<Rest>>\n  // deno-lint-ignore ban-types\n  : ExtractParam<Path, {}>\n\nexport type Payload =\n  | ArrayBuffer\n  | Blob\n  | FormData\n  | ReadableStream<unknown>\n  | Record<string, unknown>\n  | Uint8Array\n  | string\n  | undefined\n  | void\n\ntype Number = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9\n\nexport type Version = `v${\n  | Exclude<Number, 0>\n  | `${Exclude<Number, 0>}${Number}`}`\n\nexport type VersionRange =\n  | Version // exact version\n  | `> ${Version}` // greater than version\n  | `< ${Version}` // smaller than version\n  | `>= ${Version}` // greater than or equal to version\n  | `<= ${Version}` // smaller than or equal to version\n  | `${Version}...${Version}` // from (min) ... to (max)\n\nexport type Handler<\n  Pathname extends `/${string}` | unknown,\n  // deno-lint-ignore no-explicit-any\n  ParsedBody extends ObjectType | ZodString | ZodUnion<any> = never,\n  ParsedCookies extends ObjectType = never,\n  ParsedHeaders extends ObjectType = never,\n  ParsedQuery extends ObjectType = never,\n> = (\n  c: Context<\n    ExtractParams<Pathname>,\n    ParsedBody,\n    ParsedCookies,\n    ParsedHeaders,\n    ParsedQuery\n  >,\n  next: () => void,\n) => Payload | Promise<Payload>\n\nexport function handler<T>() {\n  return <\n    Pathname extends `/${string}`,\n    // deno-lint-ignore no-explicit-any\n    ValidatedBody extends ObjectType | ZodString | ZodUnion<any> = never,\n    ValidatedCookies extends ObjectType = never,\n    ValidatedHeaders extends ObjectType = never,\n    ValidatedQuery extends ObjectType = never,\n  >(\n    // deno-lint-ignore no-unused-vars\n    pathname: Pathname,\n    // deno-lint-ignore no-unused-vars\n    ...handler: (\n      | ({\n        body?: ValidatedBody\n        cookies?: ValidatedCookies\n        headers?: ValidatedHeaders\n        query?: ValidatedQuery\n        cors?: string\n        params?: Partial<Record<keyof ExtractParams<Pathname>, ZodType>>\n        gateway?: VersionRange\n      })\n      | Handler<\n        Pathname,\n        ValidatedBody,\n        ValidatedCookies,\n        ValidatedHeaders,\n        ValidatedQuery\n      >\n    )[]\n  ): T => {\n    return null as T\n  }\n}\n\nexport type BodylessHandler<\n  Pathname extends `/${string}` | unknown,\n  // deno-lint-ignore no-explicit-any\n  ParsedBody extends ObjectType | ZodString | ZodUnion<any> = never,\n  ParsedCookies extends ObjectType = never,\n  ParsedHeaders extends ObjectType = never,\n  ParsedQuery extends ObjectType = never,\n> = (\n  c: Context<\n    ExtractParams<Pathname>,\n    ParsedBody,\n    ParsedCookies,\n    ParsedHeaders,\n    ParsedQuery\n  >,\n  next: () => void,\n) => Payload | Promise<Payload>\n\nexport function bodylessHandler<T>() {\n  return <\n    Pathname extends `/${string}`,\n    ValidatedCookies extends ObjectType = never,\n    ValidatedHeaders extends ObjectType = never,\n    ValidatedQuery extends ObjectType = never,\n  >(\n    // deno-lint-ignore no-unused-vars\n    pathname: Pathname,\n    // deno-lint-ignore no-unused-vars\n    ...handler: (\n      | {\n        cookies?: ValidatedCookies\n        headers?: ValidatedHeaders\n        query?: ValidatedQuery\n        cors?: string\n        params?: Partial<Record<keyof ExtractParams<Pathname>, ZodType>>\n        gateway?: VersionRange\n      }\n      | BodylessHandler<\n        Pathname,\n        never,\n        ValidatedCookies,\n        ValidatedHeaders,\n        ValidatedQuery\n      >\n    )[]\n  ): T => {\n    return null as T\n  }\n}\n\nexport type HandlerOrSchema =\n  | {\n    body?: ZodType\n    cookies?: ObjectType\n    headers?: ObjectType\n    query?: ObjectType\n    cors?: string\n    params?: Record<string, ZodType>\n    gateway?: VersionRange\n  }\n  | Handler<unknown>\n  | BodylessHandler<unknown>\n"
  },
  {
    "path": "jwt.ts",
    "content": "// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.\nimport * as Jwt from 'djwt'\nimport { decode } from 'std/encoding/base64.ts'\nimport { Context } from './mod.ts'\n\ninterface Payload {\n  iss?: string\n  sub?: string\n  aud?: string[] | string\n  /**\n   * A `Date` object or a `number` (in seconds) when the JWT will expire.\n   */\n  exp?: Date | number\n  /**\n   * A `Date` object or a `number` (in seconds) until which the JWT will be invalid.\n   */\n  nbf?: Date | number\n  iat?: number\n  jti?: string\n  [key: string]: unknown\n}\n\nfunction importKey(key: string) {\n  return crypto.subtle.importKey(\n    'raw',\n    decode(key).buffer,\n    { name: 'HMAC', hash: 'SHA-512' },\n    true,\n    ['sign', 'verify'],\n  )\n}\n\n/**\n * Sign a payload.\n */\n// deno-lint-ignore ban-types\nexport async function sign<T extends Record<string, unknown> = {}>(\n  secret: string | Context,\n  payload: T & Payload,\n) {\n  const key = typeof secret === 'string'\n    ? await importKey(secret)\n    : await importKey(\n      (secret.env('jwt_secret') ?? secret.env('JWT_SECRET')) as string,\n    )\n\n  const { exp, nbf, ...rest } = payload\n\n  return await Jwt.create({ alg: 'HS512', typ: 'JWT' }, {\n    ...(exp && { exp: Jwt.getNumericDate(exp) }),\n    ...(nbf && { nbf: Jwt.getNumericDate(nbf) }),\n    ...rest,\n  }, key)\n}\n\n/**\n * Verify the validity of a JWT.\n */\nexport async function verify<T extends Record<string, unknown> = Payload>(\n  secret: string | Context,\n  token: string,\n  options?: Jwt.VerifyOptions,\n) {\n  try {\n    const key = typeof secret === 'string'\n      ? await importKey(secret)\n      : await importKey(\n        (secret.env('jwt_secret') ?? secret.env('JWT_SECRET')) as string,\n      )\n\n    return await Jwt.verify(token, key, options) as Jwt.Payload & T\n  } catch (_err) {\n    return\n  }\n}\n\nexport default {\n  sign,\n  verify,\n}\n"
  },
  {
    "path": "license",
    "content": "                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   Copyright 2023 Samuel Kopp\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
  },
  {
    "path": "location_data.ts",
    "content": "// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.\nimport { IncomingRequestCfProperties } from 'worker'\nimport { Context } from './context.ts'\n\ntype CloudflareRequest = Request & {\n  cf: IncomingRequestCfProperties\n}\n\n/**\n * Inspect the geolocation data of the incoming request.\n *\n * 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.\n */\nexport class LocationData {\n  #c: Context\n\n  constructor(c: Context) {\n    this.#c = c\n  }\n\n  /**\n   * The city the request originated from.\n   *\n   * @example 'Austin'\n   */\n  get city() {\n    const city = (this.#c.req.raw as CloudflareRequest).cf?.city\n\n    if (!city && this.#c.__app.proxy === 'cloudflare') {\n      return this.#c.req.headers['cf-ipcity']\n    }\n\n    return city\n  }\n\n  /**\n   * If known, the ISO 3166-2 name for the first level region associated with the IP address of the incoming request.\n   *\n   * @example 'Texas'\n   */\n  get region() {\n    return (this.#c.req.raw as CloudflareRequest).cf?.region\n  }\n\n  /**\n   * The [ISO 3166-1 Alpha 2](https://www.iso.org/iso-3166-country-codes.html) country code the request originated from.\n   *\n   * 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.\n   *\n   * If Cloudflare is unable to determine where the request originated this property is omitted.\n   *\n   * @example 'GB'\n   */\n  get country(): IncomingRequestCfProperties['country'] {\n    const country = (this.#c.req.raw as CloudflareRequest).cf?.country\n\n    if (!country && this.#c.__app.proxy === 'cloudflare') {\n      return this.#c.req\n        .headers['cf-ipcountry'] as IncomingRequestCfProperties['country']\n    }\n\n    return country\n  }\n\n  /**\n   * A two-letter code indicating the continent the request originated from.\n   *\n   * @example 'NA'\n   */\n  get continent(): IncomingRequestCfProperties['continent'] {\n    const continent = (this.#c.req.raw as CloudflareRequest).cf?.continent\n\n    if (!continent && this.#c.__app.proxy === 'cloudflare') {\n      return this.#c.req\n        .headers['cf-ipcontinent'] as IncomingRequestCfProperties['continent']\n    }\n\n    return continent\n  }\n\n  /**\n   * If known, the ISO 3166-2 code for the first-level region associated with the IP address of the incoming request.\n   *\n   * @example 'TX'\n   */\n  get regionCode(): IncomingRequestCfProperties['regionCode'] {\n    return (this.#c.req.raw as CloudflareRequest).cf?.regionCode\n  }\n\n  /**\n   * Latitude of the incoming request.\n   *\n   * @example '30.27130'\n   */\n  get latitude(): IncomingRequestCfProperties['latitude'] {\n    const latitude = (this.#c.req.raw as CloudflareRequest).cf?.latitude\n\n    if (!latitude && this.#c.__app.proxy === 'cloudflare') {\n      return this.#c.req.headers['cf-iplatitude']\n    }\n\n    return latitude\n  }\n\n  /**\n   * Longitude of the incoming request.\n   *\n   * @example '-97.74260'\n   */\n  get longitude(): IncomingRequestCfProperties['longitude'] {\n    const longitude = (this.#c.req.raw as CloudflareRequest).cf?.longitude\n\n    if (!longitude && this.#c.__app.proxy === 'cloudflare') {\n      return this.#c.req.headers['cf-iplongitude']\n    }\n\n    return longitude\n  }\n\n  /**\n   * Postal code of the incoming request.\n   *\n   * @example '78701'\n   */\n  get postalCode(): IncomingRequestCfProperties['postalCode'] {\n    return (this.#c.req.raw as CloudflareRequest).cf?.postalCode\n  }\n\n  /**\n   * Timezone of the incoming request.\n   *\n   * @example 'America/Chicago'\n   */\n  get timezone(): IncomingRequestCfProperties['timezone'] {\n    return (this.#c.req.raw as CloudflareRequest).cf?.timezone\n  }\n\n  /**\n   * The three-letter [IATA](https://en.wikipedia.org/wiki/IATA_airport_code) airport code of the data center that the request hit.\n   *\n   * @example 'DFW'\n   */\n  get datacenter(): IncomingRequestCfProperties['colo'] {\n    return (this.#c.req.raw as CloudflareRequest).cf?.colo\n  }\n}\n"
  },
  {
    "path": "mod.ts",
    "content": "// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.\nexport { cheetah as default } from './cheetah.ts'\nexport type { AppConfig, AppContext } from './cheetah.ts'\nexport { Collection } from './collection.ts'\nexport { Context } from './context.ts'\nexport { decrypt, encrypt } from './crypto.ts'\nexport { createExtension } from './extensions.ts'\nexport type { Extension } from './extensions.ts'\nexport { default as jwt } from './jwt.ts'\nexport { LocationData } from './location_data.ts'\nexport { otp } from './otp.ts'\nexport { h, Renderer } from './render.ts'\nexport { sendMail } from './send_mail.ts'\n"
  },
  {
    "path": "oauth/client.ts",
    "content": "// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.\nimport { GitHub as githubPreset, Google as googlePreset } from 'authenticus'\nimport { Preset } from 'authenticus/preset'\nimport { OAuthMethod } from './types.ts'\n\nexport type OAuthClient = {\n  name: OAuthMethod\n  // deno-lint-ignore no-explicit-any\n  preset: Preset<any, any, any>\n}\n\n/**\n * @since v1.3\n */\nexport const Google: OAuthClient = {\n  name: 'google',\n  preset: googlePreset,\n}\n\n/**\n * @since v1.3\n */\nexport const GitHub: OAuthClient = {\n  name: 'github',\n  preset: githubPreset,\n}\n"
  },
  {
    "path": "oauth/get_session_data.ts",
    "content": "// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.\nimport { getCookies } from 'std/http/cookie.ts'\nimport { Context } from '../context.ts'\nimport { verify } from '../jwt.ts'\nimport { OAuthSessionData, OAuthSessionToken } from './types.ts'\n\n/**\n * Get the data associated with the current session if logged in.\n *\n * @namespace oauth\n * @since v1.3\n */\nexport async function getSessionData(\n  c: Context,\n): Promise<OAuthSessionData | undefined> {\n  if (!c.__app.oauth) {\n    throw new Error('Please configure the oauth module for your app!')\n  }\n\n  const cookies = getCookies(c.req.raw.headers)\n\n  if (!cookies.token) {\n    return\n  }\n\n  const payload = await verify<OAuthSessionToken>(\n    c,\n    cookies.token,\n    { audience: 'oauth:session' },\n  )\n\n  if (!payload) {\n    return\n  }\n\n  if (payload.ip !== c.req.ip) {\n    await c.__app.oauth.store.delete(c, payload.identifier)\n\n    c.res.deleteCookie('token', {\n      path: c.__app.oauth.cookie?.path ?? '/',\n      ...(c.__app.oauth.cookie?.domain &&\n        { domain: c.__app.oauth.cookie.domain }),\n    })\n\n    if (typeof c.__app.oauth.onSignOut === 'function') {\n      await c.__app.oauth.onSignOut(c, payload.identifier)\n    }\n\n    return\n  }\n\n  const session = await c.__app.oauth.store.get(c, payload.identifier)\n\n  return session\n}\n"
  },
  {
    "path": "oauth/get_session_id.ts",
    "content": "// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.\nimport { getCookies } from 'std/http/cookie.ts'\nimport { Context } from '../context.ts'\nimport { verify } from '../jwt.ts'\nimport { OAuthSessionToken } from './types.ts'\n\n/**\n * Get the session identifier and verify it.\n *\n * @namespace oauth\n * @since v1.3\n */\nexport async function getSessionId(c: Context): Promise<string | undefined> {\n  if (!c.__app.oauth) {\n    throw new Error('Please configure the oauth module for your app!')\n  }\n\n  const cookies = getCookies(c.req.raw.headers)\n\n  if (!cookies.token) {\n    return\n  }\n\n  const payload = await verify<OAuthSessionToken>(\n    c,\n    cookies.token,\n    { audience: 'oauth:session' },\n  )\n\n  if (!payload) {\n    return\n  }\n\n  if (payload.ip !== c.req.ip) {\n    await c.__app.oauth.store.delete(c, payload.identifier)\n\n    c.res.deleteCookie('token', {\n      path: c.__app.oauth.cookie?.path ?? '/',\n      ...(c.__app.oauth.cookie?.domain &&\n        { domain: c.__app.oauth.cookie.domain }),\n    })\n\n    if (typeof c.__app.oauth.onSignOut === 'function') {\n      await c.__app.oauth.onSignOut(c, payload.identifier)\n    }\n\n    return\n  }\n\n  if (await c.__app.oauth.store.has(c, payload.identifier)) {\n    return payload.identifier\n  }\n}\n"
  },
  {
    "path": "oauth/get_session_token.ts",
    "content": "// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.\nimport { getCookies } from 'std/http/cookie.ts'\nimport { Context } from '../context.ts'\nimport { verify } from '../jwt.ts'\n\n/**\n * Get the session token without verifying the session.\n *\n * @namespace oauth\n * @since v1.3\n */\nexport async function getSessionToken(c: Context) {\n  const cookies = getCookies(c.req.raw.headers)\n\n  if (!cookies.token) {\n    return\n  }\n\n  const payload = await verify(\n    c,\n    cookies.token,\n    { audience: 'oauth:session' },\n  )\n\n  if (payload) {\n    return cookies.token\n  }\n}\n"
  },
  {
    "path": "oauth/handle_callback.ts",
    "content": "// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.\nimport { getNormalizedUser, getToken, getUser } from 'authenticus'\nimport { UserAgent } from 'std/http/user_agent.ts'\nimport { Context } from '../context.ts'\nimport { sign, verify } from '../jwt.ts'\nimport { LocationData } from '../location_data.ts'\nimport { OAuthClient } from './client.ts'\nimport {\n  OAuthSessionData,\n  OAuthSessionToken,\n  OAuthSignInToken,\n} from './types.ts'\n\nexport async function handleCallback(\n  c: Context,\n  client: OAuthClient,\n) {\n  if (!c.__app.oauth) {\n    throw new Error('Please configure the oauth module for your app!')\n  }\n\n  // validate request\n\n  if (\n    typeof c.req.query.state !== 'string' ||\n    typeof c.req.query.code !== 'string'\n  ) {\n    throw c.exception('Bad Request')\n  }\n\n  // validate state\n\n  const payload = await verify<OAuthSignInToken>(\n    c,\n    c.req.query.state,\n    { audience: 'oauth:sign_in' },\n  )\n\n  if (!payload || payload.ip !== c.req.ip) {\n    throw c.exception('Access Denied')\n  }\n\n  try {\n    // fetch user\n\n    const { accessToken } = await getToken(client.preset, {\n      clientId: (c.env(`${client.name.toUpperCase()}_CLIENT_ID`) ??\n        c.env(`${client.name}_client_id`)) as string,\n      clientSecret: (c.env(`${client.name.toUpperCase()}_CLIENT_SECRET`) ??\n        c.env(`${client.name}_client_secret`)) as string,\n      code: c.req.query.code,\n      redirectUri: payload.redirectUri,\n    })\n\n    const user = getNormalizedUser(\n      client.preset,\n      // @ts-ignore:\n      await getUser(client.preset, accessToken),\n    )\n\n    // create session\n\n    const identifier = crypto.randomUUID()\n\n    const expirationDate = new Date(Date.now() + 7 * 24 * 60 * 60000)\n\n    const token = await sign<OAuthSessionToken>(\n      c,\n      {\n        aud: 'oauth:session',\n        exp: expirationDate,\n        identifier,\n        ip: c.req.ip,\n      },\n    )\n\n    const userAgent = new UserAgent(c.req.headers['user-agent'] ?? '')\n\n    const location = new LocationData(c)\n\n    const data: OAuthSessionData = {\n      identifier,\n      email: user.email,\n      method: client.name,\n      userAgent: {\n        browser: userAgent.browser,\n        device: userAgent.device,\n        os: userAgent.os,\n      },\n      location: {\n        ip: c.req.ip,\n        city: location.city,\n        region: location.region,\n        regionCode: location.regionCode,\n        country: location.country,\n        continent: location.continent,\n      },\n      expiresAt: expirationDate.getTime(),\n    }\n\n    c.__app.oauth.store.set(c, identifier, data, data.expiresAt)\n\n    c.res.setCookie('token', token, {\n      expires: expirationDate,\n      httpOnly: true,\n      secure: true,\n      path: '/',\n      ...c.__app.oauth.cookie,\n    })\n\n    if (typeof c.__app.oauth.onSignIn === 'function') {\n      await c.__app.oauth.onSignIn(c, data)\n    }\n\n    return {\n      token,\n      ...data,\n    }\n  } catch (_err) {\n    throw c.exception('Bad Request')\n  }\n}\n"
  },
  {
    "path": "oauth/is_signed_in.ts",
    "content": "// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.\nimport { Context } from '../context.ts'\nimport { getSessionId } from './get_session_id.ts'\n\n/**\n * Check if the user is logged in.\n *\n * @namespace oauth\n * @since v1.3\n */\nexport async function isSignedIn(c: Context): Promise<boolean> {\n  return await getSessionId(c) !== undefined\n}\n"
  },
  {
    "path": "oauth/mod.ts",
    "content": "// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.\nexport { GitHub, Google } from './client.ts'\nexport type { OAuthClient } from './client.ts'\nexport { getSessionData } from './get_session_data.ts'\nexport { getSessionId } from './get_session_id.ts'\nexport { getSessionToken } from './get_session_token.ts'\nexport { handleCallback } from './handle_callback.ts'\nexport { isSignedIn } from './is_signed_in.ts'\nexport { signIn } from './sign_in.ts'\nexport { signOut } from './sign_out.ts'\nexport { kv, OAuthStore, upstash } from './store.ts'\nexport type { OAuthMethod, OAuthSessionData } from './types.ts'\n"
  },
  {
    "path": "oauth/sign_in.ts",
    "content": "// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.\nimport { createAuthorizeUrl } from 'authenticus'\nimport { Context } from '../context.ts'\nimport { sign } from '../jwt.ts'\nimport { OAuthClient } from './client.ts'\nimport { OAuthSignInToken } from './types.ts'\n\n/**\n * Start the login flow by redirecting the user.\n *\n * @namespace oauth\n * @since v1.3\n */\nexport async function signIn(\n  c: Context,\n  client: OAuthClient,\n  options: {\n    redirectUri: string\n    scopes?: string[]\n  },\n) {\n  if (!c.__app.oauth) {\n    throw new Error('Please configure the oauth module for your app!')\n  }\n\n  const token = await sign<OAuthSignInToken>(\n    c,\n    {\n      aud: 'oauth:sign_in',\n      exp: 300, // 5m\n      ip: c.req.ip,\n      redirectUri: options.redirectUri,\n    },\n  )\n\n  const url = createAuthorizeUrl(client.preset, {\n    clientId: c.env(`${client.name.toUpperCase()}_CLIENT_ID`) ??\n      c.env(`${client.name}_client_id`),\n    scopes: options.scopes,\n    state: token,\n    redirectUri: options.redirectUri,\n  })\n\n  c.res.redirect(url)\n}\n"
  },
  {
    "path": "oauth/sign_out.ts",
    "content": "// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.\nimport { Context } from '../context.ts'\nimport { getSessionData } from './get_session_data.ts'\n\n/**\n * Sign the user out if they're logged in.\n *\n * @namespace oauth\n * @since v1.3\n */\nexport async function signOut(c: Context) {\n  if (!c.__app.oauth) {\n    throw new Error('Please configure the oauth module for your app!')\n  }\n\n  c.res.deleteCookie('token', {\n    path: c.__app.oauth.cookie?.path ?? '/',\n    ...(c.__app.oauth.cookie?.domain &&\n      { domain: c.__app.oauth.cookie.domain }),\n  })\n\n  const data = await getSessionData(c)\n\n  if (!data) {\n    return\n  }\n\n  try {\n    await c.__app.oauth.store.delete(c, data.identifier)\n  } catch (_err) {\n    //\n  }\n\n  if (typeof c.__app.oauth.onSignOut === 'function') {\n    await c.__app.oauth.onSignOut(c, data.identifier)\n  }\n}\n"
  },
  {
    "path": "oauth/store.ts",
    "content": "// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.\nimport { Redis } from 'upstash'\nimport { KVNamespace } from 'worker'\nimport { Context } from '../context.ts'\nimport { OAuthSessionData } from './types.ts'\n\nexport class OAuthStore {\n  set: (\n    c: Context,\n    key: string,\n    value: OAuthSessionData,\n    expiresAt: number, // unix timestamp in ms\n  ) => Promise<void>\n  get: (c: Context, key: string) => Promise<OAuthSessionData | undefined>\n  delete: (c: Context, key: string) => Promise<void>\n  has: (c: Context, key: string) => Promise<boolean>\n\n  constructor(\n    options: {\n      set: OAuthStore['set']\n      get: OAuthStore['get']\n      delete: OAuthStore['delete']\n      has: OAuthStore['has']\n    },\n  ) {\n    this.set = options.set\n    this.get = options.get\n    this.delete = options.delete\n    this.has = options.has\n  }\n}\n\nlet KV: Deno.Kv | undefined\n\n/**\n * 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.\n *\n * @namespace oauth\n * @since v1.3\n */\nexport const kv = new OAuthStore({\n  async set(c, key, value, expiresAt) {\n    if (c.runtime === 'cloudflare') {\n      await (c.__app.env as { oauth: KVNamespace }).oauth.put(\n        key,\n        JSON.stringify(value),\n        { expiration: Math.round(expiresAt / 1000) },\n      )\n    } else {\n      if (!KV) {\n        KV = await Deno.openKv('oauth')\n      }\n\n      await KV.set([key], value)\n    }\n  },\n\n  async get(c, key) {\n    if (c.runtime === 'cloudflare') {\n      const kv = (c.__app.env as { oauth: KVNamespace }).oauth\n\n      const value = await kv.get<OAuthSessionData>(key, 'json')\n\n      if (value === null || value.expiresAt < Date.now()) {\n        return\n      }\n\n      return value\n    } else {\n      if (!KV) {\n        KV = await Deno.openKv('oauth')\n      }\n\n      const result = await KV.get<OAuthSessionData>([key], {\n        consistency: 'strong',\n      })\n\n      if (result.value === null) {\n        return\n      }\n\n      if (result.value.expiresAt > Date.now()) {\n        return result.value\n      }\n\n      await KV.delete([key])\n    }\n  },\n\n  async has(c, key) {\n    if (c.runtime === 'cloudflare') {\n      const kv = (c.__app.env as { oauth: KVNamespace }).oauth\n\n      const value = await kv.get<OAuthSessionData>(key, 'json')\n\n      if (value === null) {\n        return false\n      }\n\n      return value.expiresAt > Date.now()\n    } else {\n      if (!KV) {\n        KV = await Deno.openKv('oauth')\n      }\n\n      const result = await KV.get<OAuthSessionData>([key], {\n        consistency: 'strong',\n      })\n\n      if (result.value === null) {\n        return false\n      }\n\n      if (result.value.expiresAt > Date.now()) {\n        return true\n      }\n\n      await KV.delete([key])\n\n      return false\n    }\n  },\n\n  async delete(c, key) {\n    if (c.runtime === 'cloudflare') {\n      const kv = (c.__app.env as { oauth: KVNamespace }).oauth\n\n      await kv.delete(key)\n    } else {\n      if (!KV) {\n        KV = await Deno.openKv('oauth')\n      }\n\n      await KV.delete([key])\n    }\n  },\n})\n\nlet REDIS: Redis | undefined\n\n/**\n * Use [Upstash](https://upstash.com) as session storage.\n *\n * @namespace oauth\n * @since v1.3\n */\nexport const upstash = new OAuthStore({\n  async set(c, key, value, expiresAt) {\n    if (!REDIS) {\n      REDIS = new Redis({\n        url: c.env('UPSTASH_URL') as string,\n        token: c.env('UPSTASH_TOKEN') as string,\n      })\n    }\n\n    await REDIS.set(key, value, {\n      pxat: expiresAt,\n    })\n  },\n\n  async get(c, key) {\n    if (!REDIS) {\n      REDIS = new Redis({\n        url: c.env('UPSTASH_URL') as string,\n        token: c.env('UPSTASH_TOKEN') as string,\n      })\n    }\n\n    return await REDIS.get(key) ?? undefined\n  },\n\n  async has(c, key) {\n    if (!REDIS) {\n      REDIS = new Redis({\n        url: c.env('UPSTASH_URL') as string,\n        token: c.env('UPSTASH_TOKEN') as string,\n      })\n    }\n\n    return await REDIS.exists(key) === 1\n  },\n\n  async delete(c, key) {\n    if (!REDIS) {\n      REDIS = new Redis({\n        url: c.env('UPSTASH_URL') as string,\n        token: c.env('UPSTASH_TOKEN') as string,\n      })\n    }\n\n    await REDIS.del(key)\n  },\n})\n"
  },
  {
    "path": "oauth/types.ts",
    "content": "// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.\nimport { UserAgent } from 'std/http/user_agent.ts'\nimport { LocationData } from '../location_data.ts'\n\nexport type OAuthMethod =\n  | 'google'\n  | 'github'\n\nexport type OAuthSessionData = {\n  identifier: string\n  email: string\n  method: OAuthMethod\n  userAgent: {\n    browser?: UserAgent['browser']\n    device?: UserAgent['device']\n    os?: UserAgent['os']\n  }\n  location: {\n    ip: string\n    city?: LocationData['city']\n    region?: LocationData['region']\n    regionCode?: LocationData['regionCode']\n    country?: LocationData['country']\n    continent?: LocationData['continent']\n  }\n  expiresAt: number\n}\n\nexport type OAuthSessionToken = {\n  aud: 'oauth:session'\n  identifier: string\n  ip: string\n}\n\nexport type OAuthSignInToken = {\n  aud: 'oauth:sign_in'\n  ip: string\n  redirectUri: string\n}\n"
  },
  {
    "path": "otp.ts",
    "content": "// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.\nimport * as OTP from 'otpauth'\n\nexport const otp = {\n  /**\n   * Create a random secret.\n   */\n  secret(length = 64) {\n    return [...Array(length)].map(() =>\n      Math.floor(Math.random() * 16).toString(16)\n    ).join('')\n  },\n\n  /**\n   * Get the 6-digit token for a given timestamp.\n   */\n  token(secret: string, timestamp?: number) {\n    const totp = new OTP.TOTP({\n      algorithm: 'SHA1',\n      digits: 6,\n      period: 30,\n      secret: OTP.Secret.fromHex(secret),\n    })\n\n    return totp.generate({ timestamp })\n  },\n\n  /**\n   * Create a URI that you can use, for example, for a QR code to scan with Google Authenticator.\n   */\n  uri(label: string, issuer: string, secret: string) {\n    const totp = new OTP.TOTP({\n      issuer,\n      label,\n      algorithm: 'SHA1',\n      digits: 6,\n      period: 30,\n      secret: OTP.Secret.fromHex(secret),\n    })\n\n    return totp.toString()\n  },\n\n  /**\n   * Determine if a given token is valid.\n   */\n  validate(token: string, secret: string) {\n    const totp = new OTP.TOTP({\n      algorithm: 'SHA1',\n      digits: 6,\n      period: 30,\n      secret: OTP.Secret.fromHex(secret),\n    })\n\n    return totp.validate({ token }) === 0\n  },\n}\n"
  },
  {
    "path": "readme.md",
    "content": "<div align='center'>\n  <img src='https://cheetah.mod.land/cheetah.svg' width='128px' />\n  <br>\n  <br>\n  <h1>cheetah</h1>\n</div>\n\n<div align='center'>\n  <p><code>🛡️ secure</code> × <code>💎 simple</code> × <code>🪶 light</code></p>\n</div>\n\n<br>\n\n> [!WARNING]\n> cheetah is currently **not maintained**.\n\n<br>\n\n### Sneak Peek 👾\n\n```ts\nimport cheetah from 'https://deno.land/x/cheetah/mod.ts'\nimport { z } from 'https://deno.land/x/zod/mod.ts'\n\nconst app = new cheetah()\n  .post('/', {\n    body: z.object({ // < scheme validation\n      name: z.string()\n    })\n  }, async c => {\n    const body = await c.req.body()\n\n    return `Hey, ${body.name}!` // < response body\n  })\n\napp.serve() // < launch app\n```\n\n❔ Please read our [guide](https://cheetah.mod.land) or [join our Discord](https://discord.gg/2rCya9EWGv) to learn more.\n\n<br>\n\n---\n\n<div align='center'>\n  <sup>A big thank you goes to</sup>\n\n  <br>\n  <br>\n  <br>\n\n  <a href='https://deco.cx'>\n    <img src='https://github.com/azurystudio/cheetah/blob/dev/.github/sponsors/deco.svg?raw=true' height='48px' />\n    <br>\n    <br>\n    <a href='https://deco.cx'><sup><b>Build fast stores and increase sales.</b></sup></a>\n  </a>\n</div>\n\n---\n\n<br>\n\n### Release Schedule 🗓️\n\nWe strictly adhere to [SemVer](https://semver.org) and post updates **weekly**.\n\n- ◆ **current** *(e.g. v0.1.0)*\n\n  The current channel is dedicated to stable releases and is safe for use in production.\n\n- ◇ **canary** *(e.g. v0.1.0-canary.0)*\n\n  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.\n\n<br>\n\n### Contributing 😘\n\nWe appreciate your help! 💕\n\nTo contribute, please read our [contributing guidelines](https://github.com/azurystudio/cheetah/blob/dev/contributing.md)\nfirst.\n"
  },
  {
    "path": "render.ts",
    "content": "// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.\nimport { VNode } from 'preact'\nimport renderToString from 'preact/render-to-string'\nimport { brightYellow, gray } from 'std/fmt/colors.ts'\nimport { defineConfig, extract, install } from 'twind'\nimport presetAutoPrefix from 'twind/preset-autoprefix'\nimport presetTailwind from 'twind/preset-tailwind'\nimport { Context } from './mod.ts'\n\nexport function render(c: Context, Component: VNode) {\n  const htmlString = renderToString(Component)\n  try {\n    const { html, css } = extract(htmlString)\n    c.res.body = `${css.length > 0 ? `<style>${css}</style>` : ''}${html}`\n  } catch (_err) {\n    if (c.dev) {\n      console.warn(\n        gray(\n          `${\n            brightYellow('warning')\n          } - twind is not installed, thus styles might not be applied`,\n        ),\n      )\n    }\n    c.res.body = htmlString\n  }\n  c.res.header('content-type', 'text/html; charset=utf-8')\n}\n\nexport class Renderer {\n  render\n\n  constructor(options?: Parameters<typeof defineConfig>[0]) {\n    if (options) {\n      options.presets = options.presets\n        ? [presetAutoPrefix(), presetTailwind(), ...options.presets]\n        : [presetAutoPrefix(), presetTailwind()]\n    }\n\n    install(defineConfig(\n      options ?? {\n        presets: [presetAutoPrefix(), presetTailwind()],\n      },\n    ))\n\n    this.render = render\n  }\n}\n\nexport { h } from 'https://esm.sh/preact@10.17.1?target=es2022'\n"
  },
  {
    "path": "request_context.ts",
    "content": "// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.\nimport {\n  deadline as resolveWithDeadline,\n  DeadlineError,\n} from 'std/async/deadline.ts'\nimport { z, ZodStringDef, ZodType, ZodUnionDef } from 'zod'\nimport { Method } from './base.ts'\nimport { BaseType, ObjectType } from './handler.ts'\nimport { AppContext, Context } from './mod.ts'\n\ntype Static<T extends ZodType> = T extends ZodType ? z.infer<T>\n  : never\n\nexport class RequestContext<\n  Params extends Record<string, unknown> = Record<string, never>,\n  ValidatedBody extends ZodType = never,\n  ValidatedCookies extends ObjectType = never,\n  ValidatedHeaders extends ObjectType = never,\n  ValidatedQuery extends ObjectType = never,\n> {\n  #c: Record<string, string | undefined> | undefined\n  #h: Record<string, string | undefined> | undefined\n  #a: AppContext\n  #p\n  #q: Record<string, unknown> | undefined\n  #r\n  #s\n  #e\n\n  constructor(\n    a: AppContext,\n    p: Record<string, string | undefined>,\n    r: Request,\n    s: {\n      body?: ZodType | undefined\n      cookies?: ObjectType | undefined\n      headers?: ObjectType | undefined\n      query?: ObjectType | undefined\n      params?: Record<string, ZodType>\n      [key: string]: unknown\n    } | null,\n    e: Context['exception'],\n  ) {\n    this.#a = a\n    this.#p = p\n    this.#r = r\n    this.#s = s\n    this.#e = e\n  }\n\n  get gateway(): number {\n    return this.#a.gateway ?? -1\n  }\n\n  get ip(): string {\n    return this.#a.ip\n  }\n\n  /**\n   * The method of the incoming request.\n   *\n   * @example 'GET'\n   * @since v0.12\n   */\n  get method() {\n    return this.#r.method as Uppercase<Method>\n  }\n\n  /**\n   * A method to retrieve the corresponding value of a parameter.\n   */\n  param<T extends keyof Params>(name: T): Params[T] {\n    if (this.#s?.params && this.#s.params[name as string]) {\n      const result = this.#s.params[name as string].safeParse(this.#p[name])\n\n      if (!result.success) {\n        throw this.#e('Bad Request')\n      }\n\n      return result.data as Params[T]\n    } else {\n      return this.#p[name as string] as Params[T]\n    }\n  }\n\n  /**\n   * Retrieve the original request object.\n   *\n   * @since v1.0\n   */\n  get raw() {\n    return this.#r\n  }\n\n  /**\n   * The validated body of the incoming request.\n   */\n  async body(options?: {\n    /**\n     * This enables the conversion of a FormData request body into a JSON object (if the request body has the MIME type `multipart/form-data`).\n     *\n     * @default false\n     */\n    transform: boolean\n  }): Promise<\n    [ValidatedBody] extends [never] ? unknown : Static<ValidatedBody>\n  > {\n    if (!this.#s?.body) {\n      // @ts-ignore:\n      return undefined\n    }\n\n    let body\n\n    try {\n      if (\n        (this.#s.body as BaseType<ZodStringDef>)._def.typeName ===\n          'ZodString' ||\n        (this.#s.body as BaseType<ZodUnionDef>)._def.typeName === 'ZodUnion' &&\n          (this.#s.body as BaseType<ZodUnionDef>)._def.options.every((\n            { _def },\n          ) => _def.typeName === 'ZodString')\n      ) {\n        body = await resolveWithDeadline(this.#r.text(), 2500)\n      } else {\n        if (\n          (options?.transform === true || this.#s?.transform === true) &&\n          this.#r.headers.get('content-type') === 'multipart/form-data'\n        ) {\n          const formData = await resolveWithDeadline(this.#r.formData(), 2500)\n\n          body = {} as Record<string, unknown>\n\n          for (const [key, value] of formData.entries()) {\n            body[key] = value\n          }\n        } else {\n          body = await resolveWithDeadline(this.#r.json(), 2500)\n        }\n      }\n    } catch (err: unknown) {\n      throw this.#e(\n        err instanceof DeadlineError ? 'Content Too Large' : 'Bad Request',\n      )\n    }\n\n    const result = this.#s.body.safeParse(body)\n\n    if (!result.success) {\n      throw this.#e('Bad Request')\n    }\n\n    return result.data\n  }\n\n  /**\n   * The validated cookies of the incoming request.\n   */\n  get cookies(): [ValidatedCookies] extends [never] ? never\n    : Static<ValidatedCookies> {\n    if (this.#c || !this.#s?.cookies) {\n      return this.#c as [ValidatedCookies] extends [never] ? never\n        : Static<ValidatedCookies>\n    }\n\n    try {\n      const header = this.#r.headers.get('cookies') ?? ''\n\n      if (header.length > 1000) {\n        throw this.#e('Content Too Large')\n      }\n\n      this.#c = header\n        .split(/;\\s*/)\n        .map((pair) => pair.split(/=(.+)/))\n        .reduce((acc: Record<string, string>, [k, v]) => {\n          acc[k] = v\n\n          return acc\n        }, {})\n\n      delete this.#c['']\n    } catch (_err) {\n      this.#c = {}\n    }\n\n    const isValid = this.#s.cookies.safeParse(this.#c).success\n\n    if (!isValid) {\n      throw this.#e('Bad Request')\n    }\n\n    return this.#c as [ValidatedCookies] extends [never] ? never\n      : Static<ValidatedCookies>\n  }\n\n  /**\n   * The validated headers of the incoming request.\n   */\n  get headers(): [ValidatedHeaders] extends [never]\n    ? Record<string, string | undefined>\n    : Static<ValidatedHeaders> {\n    if (this.#h) {\n      return this.#h as [ValidatedHeaders] extends [never]\n        ? Record<string, string | undefined>\n        : Static<ValidatedHeaders>\n    }\n\n    this.#h = {}\n\n    let num = 0\n\n    for (const [key, value] of this.#r.headers) {\n      if (num === 50) {\n        break\n      }\n\n      if (!this.#h[key.toLowerCase()]) {\n        this.#h[key.toLowerCase()] = value\n      }\n\n      num++\n    }\n\n    if (this.#s?.headers) {\n      const isValid = this.#s.headers.safeParse(this.#h).success\n\n      if (!isValid) {\n        throw this.#e('Bad Request')\n      }\n    }\n\n    return this.#h as [ValidatedHeaders] extends [never]\n      ? Record<string, string | undefined>\n      : Static<ValidatedHeaders>\n  }\n\n  /**\n   * The validated query parameters of the incoming request.\n   */\n  get query(): [ValidatedQuery] extends [never] ? Record<string, unknown>\n    : Static<ValidatedQuery> {\n    if (this.#q) {\n      return this.#q as [ValidatedQuery] extends [never]\n        ? Record<string, unknown>\n        : Static<ValidatedQuery>\n    }\n\n    this.#q = {}\n\n    if (this.#a.request.querystring) {\n      const arr = this.#a.request.querystring.split('&')\n\n      for (let i = 0; i < arr.length; i++) {\n        const [key, value] = arr[i].split('=')\n\n        if (!key) {\n          continue\n        }\n\n        if (typeof value === 'undefined') {\n          this.#q[key] = true\n\n          continue\n        }\n\n        try {\n          this.#q[key] = JSON.parse(decodeURIComponent(value))\n        } catch (_err) {\n          this.#q[key] = decodeURIComponent(value)\n        }\n      }\n    }\n\n    if (this.#s?.query) {\n      const isValid = this.#s.query.safeParse(this.#q).success\n\n      if (!isValid) {\n        throw this.#e('Bad Request')\n      }\n    }\n\n    return this.#q as [ValidatedQuery] extends [never] ? Record<string, unknown>\n      : Static<ValidatedQuery>\n  }\n\n  /**\n   * Parse the request body as an `ArrayBuffer` with a set time limit in ms.\n   *\n   * @param deadline (default `2500`)\n   */\n  async blob(deadline = 2500) {\n    try {\n      const promise = this.#r.blob()\n\n      return await resolveWithDeadline(promise, deadline)\n    } catch (_err) {\n      return null\n    }\n  }\n\n  /**\n   * Parse the request body as an `ArrayBuffer` with a set time limit in ms.\n   *\n   * @param deadline (default `2500`)\n   */\n  async buffer(deadline = 2500) {\n    try {\n      const promise = this.#r.arrayBuffer()\n\n      return await resolveWithDeadline(promise, deadline)\n    } catch (_err) {\n      return null\n    }\n  }\n\n  /**\n   * Parse the request body as JSON with a set time limit in ms.\n   *\n   * **If you have defined a validation schema, use `c.req.body()` instead!**\n   *\n   * @param deadline (default `2500`)\n   */\n  async json(deadline = 2500): Promise<unknown> {\n    try {\n      const promise = this.#r.json()\n\n      return await resolveWithDeadline(promise, deadline)\n    } catch (_err) {\n      return null\n    }\n  }\n\n  /**\n   * Parse the request body as a `FormData` object with a set time limit in ms.\n   *\n   * @param deadline (default `2500`)\n   */\n  async formData(deadline = 2500) {\n    try {\n      const promise = this.#r.formData()\n\n      return await resolveWithDeadline(promise, deadline)\n    } catch (_err) {\n      return null\n    }\n  }\n\n  /**\n   * Parse the request body as a `string` with a set time limit in ms.\n   *\n   * **If you have defined a validation schema, use `c.req.body()` instead!**\n   *\n   * @param deadline (default `2500`)\n   */\n  async text(deadline = 2500) {\n    try {\n      const promise = this.#r.text()\n\n      return await resolveWithDeadline(promise, deadline)\n    } catch (_err) {\n      return null\n    }\n  }\n\n  /**\n   * A readable stream of the request body.\n   */\n  get stream() {\n    return this.#r.body\n  }\n}\n"
  },
  {
    "path": "response_context.ts",
    "content": "// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.\nimport { Payload } from './handler.ts'\n\nexport class ResponseContext {\n  #i\n\n  constructor(__internal: {\n    b: Exclude<Payload, void> | null\n    c: number\n    h: Headers\n  }) {\n    this.#i = __internal\n  }\n\n  get body(): Exclude<Payload, void> | null {\n    return this.#i.b\n  }\n\n  set body(data: Exclude<Payload, void> | null) {\n    this.#i.b = data\n  }\n\n  /**\n   * The size of the response body (`-1` if it cannot be calculated).\n   */\n  get bodySize() {\n    if (\n      this.#i.b === null\n    ) {\n      return 0\n    }\n\n    let s\n\n    switch (this.#i.b.constructor.name) {\n      case 'String': {\n        s = (this.#i.b as string).length\n        break\n      }\n\n      case 'Object': {\n        s = JSON.stringify(this.#i.b).length\n        break\n      }\n\n      case 'ArrayBuffer': {\n        s = (this.#i.b as ArrayBuffer).byteLength\n        break\n      }\n\n      case 'Uint8Array': {\n        s = (this.#i.b as ArrayBuffer).byteLength\n        break\n      }\n\n      case 'Blob': {\n        s = (this.#i.b as Blob).size\n        break\n      }\n\n      default: {\n        s = -1\n        break\n      }\n    }\n\n    return s\n  }\n\n  /**\n   * The status code of the response.\n   */\n  get code() {\n    return this.#i.c\n  }\n\n  set code(code: number) {\n    this.#i.c = code\n  }\n\n  /**\n   * Attach a cookie to the response.\n   *\n   * @since v1.4\n   */\n  setCookie(name: string, value: string, options?: {\n    expires?: Date\n    maxAge?: number\n    domain?: string\n    path?: string\n    secure?: boolean\n    httpOnly?: boolean\n    sameSite?:\n      | 'strict'\n      | 'lax'\n      | 'none'\n  }) {\n    let cookie = `${name}=${value};`\n\n    this.#i.h.append(\n      'Set-Cookie',\n      (\n        options?.expires &&\n        (cookie += ` Expires=${options.expires.toUTCString()};`),\n          options?.maxAge && (cookie += ` Max-Age=${options.maxAge};`),\n          options?.domain && (cookie += ` Domain=${options.domain};`),\n          options?.path && (cookie += ` Path=${options.path};`),\n          options?.secure && (cookie += ' Secure;'),\n          options?.httpOnly && (cookie += ' HttpOnly;'),\n          options?.sameSite &&\n          (cookie += ` SameSite=${\n            options.sameSite.charAt(0).toUpperCase() +\n            options.sameSite.slice(1)\n          };`),\n          cookie\n      ),\n    )\n  }\n\n  /**\n   * Set an empty `Set-Cookie` header to delete the cookie.\n   *\n   * @since v1.4\n   */\n  deleteCookie(name: string, options?: { path?: string; domain?: string }) {\n    this.setCookie(name, '', {\n      expires: new Date(0),\n      ...options,\n    })\n  }\n\n  /**\n   * Attach a header to the response.\n   */\n  header(name: string, value: string | undefined) {\n    if (value === undefined) {\n      this.#i.h.delete(name)\n    } else {\n      this.#i.h.set(name, value)\n    }\n  }\n\n  /**\n   * Redirect the incoming request.\n   *\n   * @param destination e.g. https://google.com\n   * @param code e.g. 301, default 307\n   */\n  redirect(destination: string, code = 307) {\n    this.#i.c = code\n    this.#i.h.set('location', destination)\n  }\n}\n"
  },
  {
    "path": "send_mail.ts",
    "content": "// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.\ntype MailContact =\n  | { name?: string; email: string }\n  | { name?: string; email: string }[]\n  | string\n  | string[]\n\n/**\n * Send an email through [mailchannels](https://blog.cloudflare.com/sending-email-from-workers-with-mailchannels).\n */\nexport function sendMail(\n  options: {\n    subject: string\n    message: string\n    from: {\n      name: string\n      email: string\n    }\n    to: MailContact\n    cc?: MailContact\n    bcc?: MailContact\n    reply?: boolean\n    dkim?: {\n      domain?: string\n      privateKey?: string\n      selector?: string\n    }\n  },\n) {\n  // to\n  const to: { name?: string; email: string }[] = []\n\n  if (typeof options.to === 'string') {\n    to.push({ email: options.to })\n  } else if (options.to instanceof Array) {\n    for (const recipient of options.to) {\n      to.push(typeof recipient === 'string' ? { email: recipient } : recipient)\n    }\n  } else {\n    to.push(options.to)\n  }\n\n  // cc\n  const cc: { name?: string; email: string }[] = []\n\n  if (typeof options.cc === 'string') {\n    cc.push({ email: options.cc })\n  } else if (options.cc instanceof Array) {\n    for (const recipient of options.cc) {\n      cc.push(typeof recipient === 'string' ? { email: recipient } : recipient)\n    }\n  } else if (options.cc) {\n    cc.push(options.cc)\n  }\n\n  // bcc\n  const bcc: { name?: string; email: string }[] = []\n\n  if (typeof options.bcc === 'string') {\n    bcc.push({ email: options.bcc })\n  } else if (options.bcc instanceof Array) {\n    for (const recipient of options.bcc) {\n      bcc.push(\n        typeof recipient === 'string' ? { email: recipient } : recipient,\n      )\n    }\n  } else if (options.bcc) {\n    bcc.push(options.bcc)\n  }\n\n  return fetch('https://api.mailchannels.net/tx/v1/send', {\n    method: 'POST',\n    headers: {\n      'content-type': 'application/json',\n    },\n    body: JSON.stringify({\n      personalizations: [{\n        to,\n        ...(cc.length > 0 && { cc }),\n        ...(bcc.length > 0 && { bcc }),\n        ...(options.dkim?.domain && { dkim_domain: options.to }),\n        ...(options.dkim?.privateKey && { dkim_private_key: options.to }),\n        ...(options.dkim?.selector && { dkim_selector: options.to }),\n      }],\n      from: options.from,\n      subject: options.subject,\n      content: [{\n        type: options.message.startsWith('<html>') ? 'text/html' : 'text/plain',\n        value: options.message,\n      }],\n      ...(options.reply && { reply_to: to[0] }),\n    }),\n  })\n}\n"
  },
  {
    "path": "test/context.test.ts",
    "content": "// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.\nimport { assertEquals } from 'std/assert/mod.ts'\nimport cheetah from '../mod.ts'\n\nDeno.test('Context', async (t) => {\n  Deno.env.set('cheetah_test', 'test')\n\n  const app = new cheetah()\n\n  await t.step('c.runtime', async () => {\n    app.get('/runtime', (c) => c.runtime)\n    assertEquals(\n      await (await app.fetch(new Request('http://localhost/runtime')))\n        .text(),\n      'deno',\n    )\n  })\n})\n"
  },
  {
    "path": "test/cors.test.ts",
    "content": "// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.\nimport { assertEquals } from 'std/assert/mod.ts'\nimport cheetah from '../mod.ts'\n\nDeno.test('CORS', async (t) => {\n  const app = new cheetah({\n    cors: '*',\n  })\n\n  await t.step('Global CORS', async () => {\n    app.get('/global', () => 'test')\n\n    const result = await app.fetch(\n      new Request('http://localhost/global', {\n        method: 'OPTIONS',\n        headers: {\n          origin: 'custom.com',\n          'access-control-request-method': 'GET',\n        },\n      }),\n    )\n\n    assertEquals(result.headers.get('access-control-allow-origin'), '*')\n  })\n\n  await t.step('Per-Route CORS', async () => {\n    app.get('/per-route', { cors: 'custom.com' }, () => 'test')\n\n    const result = await app.fetch(\n      new Request('http://localhost/per-route', {\n        method: 'OPTIONS',\n        headers: {\n          origin: 'foobar.com',\n          'access-control-request-method': 'GET',\n        },\n      }),\n    )\n\n    assertEquals(\n      result.headers.get('access-control-allow-origin'),\n      'custom.com',\n    )\n  })\n})\n"
  },
  {
    "path": "test/exception.test.ts",
    "content": "// TODO create test suite for c.exception\n"
  },
  {
    "path": "test/ext/favicon.test.ts",
    "content": "// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.\nimport { assertEquals } from 'std/assert/mod.ts'\nimport { favicon } from '../../ext/favicon.ts'\nimport cheetah from '../../mod.ts'\n\nDeno.test('ext/favicon', async () => {\n  const app = new cheetah()\n\n  app.use(favicon({\n    source: await Deno.readFile('./.github/cheetah.svg'),\n  }))\n\n  app.get('/', () => 'Hello World')\n\n  const res = await app.fetch(new Request('http://localhost/favicon.ico'))\n\n  assertEquals(\n    await res.text(),\n    await Deno.readTextFile('./.github/cheetah.svg'),\n  )\n\n  assertEquals(\n    res.status,\n    200,\n  )\n})\n"
  },
  {
    "path": "test/ext/files.test.ts",
    "content": "// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.\nimport { assertEquals } from 'std/assert/mod.ts'\nimport { files } from '../../ext/files.ts'\nimport cheetah from '../../mod.ts'\n\nDeno.test('ext/files', async (t) => {\n  await t.step('root', async () => {\n    const app = new cheetah()\n\n    app.use(files({\n      serve: {\n        directory: './test',\n      },\n    }))\n\n    app.get('/', () => 'Hello World')\n\n    const res1 = await app.fetch(new Request('http://localhost'))\n\n    assertEquals(\n      await res1.text(),\n      'Hello World',\n    )\n\n    assertEquals(\n      res1.status,\n      200,\n    )\n\n    const res2 = await app.fetch(\n      new Request('http://localhost/render.test.tsx'),\n    )\n\n    assertEquals(\n      await res2.text(),\n      await Deno.readTextFile('./test/render.test.tsx'),\n    )\n\n    assertEquals(\n      res2.status,\n      200,\n    )\n  })\n\n  await t.step('with prefix', async () => {\n    const app = new cheetah()\n\n    app.use(\n      '/t',\n      files({\n        serve: {\n          directory: './test',\n        },\n      }),\n    )\n\n    app.get('/', () => 'Hello World')\n\n    const res1 = await app.fetch(new Request('http://localhost'))\n\n    assertEquals(\n      await res1.text(),\n      'Hello World',\n    )\n\n    assertEquals(\n      res1.status,\n      200,\n    )\n\n    const res2 = await app.fetch(\n      new Request('http://localhost/t/ext/pretty.test.ts'),\n    )\n\n    assertEquals(\n      await res2.text(),\n      await Deno.readTextFile('./test/ext/pretty.test.ts'),\n    )\n\n    assertEquals(\n      res2.status,\n      200,\n    )\n  })\n})\n"
  },
  {
    "path": "test/ext/firewall.test.ts",
    "content": "import { assertEquals } from 'std/assert/mod.ts'\nimport { firewall } from '../../ext/firewall.ts'\nimport cheetah from '../../mod.ts'\n\nDeno.test('ext/firewall', async (t) => {\n  await t.step('vpn', async () => {\n    const app = new cheetah({ proxy: 'cloudflare' })\n\n    app.use(firewall({\n      blockVPN: true,\n    }))\n\n    const res = await app.fetch(\n      new Request('http://localhost', {\n        headers: {\n          'cf-connecting-ip': '2.56.16.0',\n        },\n      }),\n    )\n\n    assertEquals(res.status, 403)\n  })\n})\n\nDeno.test('datacenters', async () => {\n  const app = new cheetah({ proxy: 'cloudflare' })\n\n  app.use(firewall({\n    blockDatacenter: true,\n  }))\n\n  const res = await app.fetch(\n    new Request('http://localhost', {\n      headers: {\n        'cf-connecting-ip': '1.12.32.0',\n      },\n    }),\n  )\n\n  assertEquals(res.status, 403)\n})\n\nDeno.test('customRanges', async () => {\n  const app = new cheetah({ proxy: 'cloudflare' })\n\n  app.use(firewall({\n    customRanges: [\n      '1.2.3.4/32',\n    ],\n  }))\n\n  const res = await app.fetch(\n    new Request('http://localhost', {\n      headers: {\n        'cf-connecting-ip': '1.2.3.4',\n      },\n    }),\n  )\n\n  assertEquals(res.status, 403)\n})\n"
  },
  {
    "path": "test/ext/pretty.test.ts",
    "content": "// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.\nimport { assertEquals } from 'std/assert/mod.ts'\nimport { pretty } from '../../ext/pretty.ts'\nimport cheetah from '../../mod.ts'\n\nDeno.test('ext/pretty', async (t) => {\n  const obj = {\n    lastname: 'Doe',\n    firstname: 'John',\n    data: {\n      height: 180,\n      age: 20,\n    },\n  }\n\n  await t.step('indent', async () => {\n    const app = new cheetah()\n      .use(pretty({\n        indentSize: 4,\n        sort: false,\n      }))\n\n    app.get('/', () => obj)\n\n    const result = await app.fetch(new Request('http://localhost'))\n\n    assertEquals(\n      await result.text(),\n      JSON.stringify(\n        {\n          lastname: 'Doe',\n          firstname: 'John',\n          data: {\n            height: 180,\n            age: 20,\n          },\n        },\n        null,\n        4,\n      ),\n    )\n\n    assertEquals(\n      result.headers.get('content-type'),\n      'application/json; charset=utf-8',\n    )\n  })\n\n  await t.step('sort', async () => {\n    const app = new cheetah()\n      .use(pretty())\n\n    app.get('/', () => obj)\n\n    const result = await app.fetch(new Request('http://localhost'))\n\n    assertEquals(\n      await result.text(),\n      JSON.stringify(\n        {\n          data: {\n            age: 20,\n            height: 180,\n          },\n          firstname: 'John',\n          lastname: 'Doe',\n        },\n        null,\n        2,\n      ),\n    )\n\n    assertEquals(\n      result.headers.get('content-type'),\n      'application/json; charset=utf-8',\n    )\n  })\n})\n"
  },
  {
    "path": "test/extensions.test.ts",
    "content": "// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.\nimport { assertEquals } from 'std/assert/mod.ts'\nimport cheetah, { createExtension } from '../mod.ts'\n\nDeno.test('Extensions', async (t) => {\n  let count = 0\n\n  const a = createExtension({\n    onPlugIn({ setRoute }) {\n      setRoute('get', '/cookie', () => '🍪')\n\n      count++\n    },\n    onRequest({ req }) {\n      req.headers.set('custom', 'test')\n    },\n  })\n\n  const b = createExtension({\n    onResponse({ c, prefix }) {\n      c.res.body = 'custom'\n\n      assertEquals(prefix, '/b')\n    },\n  })\n\n  const c = createExtension({\n    onRequest({ req }) {\n      const url = new URL(req.url)\n\n      if (url.pathname.startsWith('/foo')) {\n        return new Response('hello')\n      }\n    },\n  })\n\n  const app = new cheetah()\n    .use(a(), c())\n    .use('/b', b())\n    .get('/a', (c) => {\n      assertEquals(c.req.headers.custom, 'test')\n\n      return 'test'\n    })\n    .get('/b', (c) => {\n      assertEquals(c.req.headers.custom, 'test')\n\n      return 'test'\n    })\n\n  await t.step('onPlugIn (part 1)', async () => {\n    const result = await app.fetch(new Request('http://localhost/cookie'))\n\n    assertEquals(await result.text(), '🍪')\n  })\n\n  await t.step('onRequest', async () => {\n    const result = await app.fetch(new Request('http://localhost/a'))\n\n    assertEquals(await result.text(), 'test')\n  })\n\n  await t.step('onResponse', async () => {\n    const result1 = await app.fetch(new Request('http://localhost/b'))\n\n    assertEquals(await result1.text(), 'custom')\n\n    const result2 = await app.fetch(new Request('http://localhost/foo/bar'))\n\n    assertEquals(await result2.text(), 'hello')\n  })\n\n  await t.step('onPlugIn (part 2)', async () => {\n    const result = await app.fetch(new Request('http://localhost/cookie'))\n\n    assertEquals(await result.text(), '🍪')\n    assertEquals(count, 1)\n  })\n})\n"
  },
  {
    "path": "test/jwt.test.ts",
    "content": "// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.\nimport { assertEquals } from 'std/assert/mod.ts'\nimport { cheetah } from '../cheetah.ts'\nimport { createJwtSecret } from '../cli/cmd/random/create_jwt_secret.ts'\nimport { jwt } from '../mod.ts'\n\nDeno.test('jwt', async () => {\n  const key = await createJwtSecret()\n\n  const token = await jwt.sign(key, { example: 'object' })\n\n  assertEquals(\n    await jwt.verify(await createJwtSecret(), token) === undefined,\n    true,\n  )\n  assertEquals(await jwt.verify(key, token) !== undefined, true)\n\n  Deno.env.set('jwt_secret', key)\n\n  const app = new cheetah()\n\n  app.get('/one', async (c) => {\n    assertEquals(await jwt.verify(c, token) !== undefined, true)\n  })\n\n  await app.fetch(new Request('http://localhost/one'))\n\n  Deno.env.delete('jwt_secret')\n  Deno.env.set('JWT_SECRET', key)\n\n  app.get('/two', async (c) => {\n    assertEquals(await jwt.verify(c, token) !== undefined, true)\n  })\n\n  await app.fetch(new Request('http://localhost/two'))\n})\n"
  },
  {
    "path": "test/many_handlers.test.ts",
    "content": "// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.\nimport { assertEquals } from 'std/assert/mod.ts'\nimport cheetah from '../mod.ts'\n\nDeno.test('Many Handlers', async () => {\n  const app = new cheetah()\n\n  app.get('/a', (_, next) => {\n    next()\n\n    return 'a'\n  }, (c) => {\n    if (c.req.headers.foo !== undefined) {\n      return 'b'\n    }\n  })\n\n  app.get('/b', (c) => {\n    c.res.header('foo', 'bar')\n  }, (c) => {\n    c.res.header('foo', 'foo')\n  })\n\n  app.get('/c', (c, next) => {\n    c.res.header('foo', 'bar')\n\n    next()\n  }, (c) => {\n    c.res.header('foo', 'foo')\n  })\n\n  assertEquals(\n    await (await app.fetch(new Request('http://localhost/a'))).text(),\n    'a',\n  )\n\n  assertEquals(\n    await (await app.fetch(\n      new Request('http://localhost/a', { headers: { 'foo': 'bar' } }),\n    )).text(),\n    'b',\n  )\n\n  assertEquals(\n    (await app.fetch(new Request('http://localhost/b'))).headers.get('foo'),\n    'bar',\n  )\n\n  assertEquals(\n    (await app.fetch(new Request('http://localhost/c'))).headers.get('foo'),\n    'foo',\n  )\n})\n"
  },
  {
    "path": "test/preflight_mode.test.ts",
    "content": "// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.\nimport { assertEquals } from 'std/assert/mod.ts'\nimport cheetah from '../mod.ts'\n\nDeno.test('preflight mode', async (t) => {\n  await t.step('basic', async () => {\n    const app = new cheetah({ preflight: true })\n\n    app.get('/foo1', () => {\n      return 'bar1'\n    })\n\n    app.post('/foo2', () => {\n      return 'bar2'\n    })\n\n    const res1 = await app.fetch(new Request('https://deno.com/foo1'))\n\n    assertEquals(res1.body === null, false)\n\n    const res2 = await app.fetch(\n      new Request('https://deno.com/foo1', { method: 'HEAD' }),\n    )\n\n    assertEquals(res2.body === null, true)\n\n    const res3 = await app.fetch(\n      new Request('https://deno.com/foo2', { method: 'POST' }),\n    )\n\n    assertEquals(res3.body === null, false)\n\n    const res4 = await app.fetch(\n      new Request('https://deno.com/foo2', { method: 'HEAD' }),\n    )\n\n    assertEquals(res4.status, 404)\n    assertEquals(res4.body === null, true)\n  })\n\n  await t.step('not found', async () => {\n    const app = new cheetah({ preflight: true })\n\n    const res1 = await app.fetch(new Request('https://deno.com/foo'))\n\n    assertEquals(res1.body === null, false)\n\n    const res2 = await app.fetch(\n      new Request('https://deno.com/foo', { method: 'HEAD' }),\n    )\n\n    assertEquals(res2.body === null, true)\n  })\n\n  await t.step('not found (custom)', async () => {\n    const app = new cheetah({\n      preflight: true,\n      notFound() {\n        return new Response('custom', {\n          headers: {\n            foo: 'bar',\n          },\n        })\n      },\n    })\n\n    const res1 = await app.fetch(new Request('https://deno.com'))\n\n    assertEquals(res1.body === null, false)\n    assertEquals(res1.headers.get('foo'), 'bar')\n\n    const res2 = await app.fetch(\n      new Request('https://deno.com', { method: 'HEAD' }),\n    )\n\n    assertEquals(res2.body === null, true)\n    assertEquals(res2.headers.get('foo'), 'bar')\n  })\n\n  await t.step('error', async () => {\n    const app = new cheetah({ preflight: true })\n\n    app.get('/foo', () => {\n      throw new Error()\n    })\n\n    const res1 = await app.fetch(new Request('https://deno.com/foo'))\n\n    assertEquals(res1.body === null, false)\n\n    const res2 = await app.fetch(\n      new Request('https://deno.com/foo', { method: 'HEAD' }),\n    )\n\n    assertEquals(res2.body === null, true)\n  })\n\n  await t.step('error (custom)', async () => {\n    const app = new cheetah({\n      preflight: true,\n      error() {\n        return new Response('custom', {\n          headers: {\n            foo: 'bar',\n          },\n        })\n      },\n    })\n\n    app.get('/foo', () => {\n      throw new Error()\n    })\n\n    const res1 = await app.fetch(new Request('https://deno.com/foo'))\n\n    assertEquals(res1.body === null, false)\n    assertEquals(res1.headers.get('foo'), 'bar')\n\n    const res2 = await app.fetch(\n      new Request('https://deno.com/foo', { method: 'HEAD' }),\n    )\n\n    assertEquals(res2.body === null, true)\n    assertEquals(res2.headers.get('foo'), 'bar')\n  })\n})\n"
  },
  {
    "path": "test/render.test.tsx",
    "content": "// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.\n/** @jsx h */\nimport { DOMParser } from 'dom'\nimport { Fragment, h, VNode } from 'preact'\nimport { assert, assertEquals } from 'std/assert/mod.ts'\nimport { z } from 'zod'\nimport cheetah, { Renderer } from '../mod.ts'\n\nconst Document = ({ children }: { children: VNode }) => {\n  return (\n    <html>\n      <head>\n        <title>This is a document!</title>\n      </head>\n      <body>\n        {children}\n      </body>\n    </html>\n  )\n}\n\nconst Styled = () => {\n  return (\n    <Document>\n      <h3 class='text-sm italic' id='styled'>\n        styled <code class='font-mono'>h3</code> component\n      </h3>\n    </Document>\n  )\n}\n\nconst Unstyled = () => {\n  return (\n    <Document>\n      <h3 id='unstyled'>\n        unstyled <code>h3</code> component\n      </h3>\n    </Document>\n  )\n}\n\nconst MetaTagsWithoutWrappers = () => {\n  return (\n    <Fragment>\n      <title>This is a document!</title>\n      <meta charSet='utf-8' />\n      <h1>Hello world!</h1>\n    </Fragment>\n  )\n}\n\nconst app = new cheetah()\n\nconst { render } = new Renderer()\n\napp.get('/render', {\n  query: z.object({\n    type: z.union([\n      z.literal('styled'),\n      z.literal('unstyled'),\n      z.literal('meta-tags-without-wrappers'),\n    ]),\n  }),\n}, (c) => {\n  const type = c.req.query.type\n\n  if (type === 'meta-tags-without-wrappers') {\n    return render(c, <MetaTagsWithoutWrappers />)\n  }\n\n  render(\n    c,\n    type === 'styled' ? <Styled /> : <Unstyled />,\n  )\n})\n\nDeno.test('render', async (test) => {\n  await test.step('Twind styles are applied to the resulting HTML correctly.', async () => {\n    const renderResponse = await app.fetch(\n      new Request('http://localhost/render?type=styled'),\n    )\n\n    const htmlText = await renderResponse.text()\n\n    const document = new DOMParser().parseFromString(\n      htmlText,\n      'text/html',\n    )\n\n    assert(document)\n    assert([...document.getElementsByTagName('style')].length === 1)\n    assertEquals(\n      document.getElementById('styled')?.innerText,\n      'styled h3 component',\n    )\n    assertEquals(\n      renderResponse.headers.get('content-type'),\n      'text/html; charset=utf-8',\n    )\n  })\n\n  const renderResponse = await app.fetch(\n    new Request('http://localhost/render?type=unstyled'),\n  )\n\n  const htmlText = await renderResponse.text()\n\n  const document = new DOMParser().parseFromString(\n    htmlText,\n    'text/html',\n  )\n\n  assert(document)\n\n  await test.step('No empty style tag is injected if no Twind styles are utilised.', () => {\n    assertEquals(\n      [...document.getElementsByTagName('style')].length,\n      0,\n    )\n    assertEquals(\n      document.getElementById('unstyled')?.innerText,\n      'unstyled h3 component',\n    )\n    assertEquals(\n      renderResponse.headers.get('content-type'),\n      'text/html; charset=utf-8',\n    )\n  })\n\n  await test.step('Head meta tags are able to be injected into the HTML output properly.', () => {\n    const headElementsInDocument = [...document.getElementsByTagName('head')]\n\n    assert(headElementsInDocument.length === 1)\n\n    const headElementInDocument = headElementsInDocument.at(0)\n\n    assert(headElementInDocument)\n    assert(\n      [...headElementInDocument.children].find((childNode) =>\n        childNode.tagName === 'TITLE' &&\n        childNode.textContent === 'This is a document!'\n      ),\n    )\n  })\n\n  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 () => {\n    const renderResponse = await app.fetch(\n      new Request('http://localhost/render?type=meta-tags-without-wrappers'),\n    )\n\n    const htmlText = await renderResponse.text()\n\n    const document = new DOMParser().parseFromString(\n      htmlText,\n      'text/html',\n    )\n    assert(document)\n\n    const { documentElement } = document\n    assert(documentElement)\n\n    const headElementsInDocument = documentElement.getElementsByTagName('head')\n    const [headElement] = headElementsInDocument\n    assert(headElement)\n\n    const titleElementInDocument = headElement.getElementsByTagName('title').at(\n      0,\n    )\n    assert(\n      titleElementInDocument &&\n        titleElementInDocument.textContent === 'This is a document!',\n    )\n\n    const metaTagsInDocument = documentElement.getElementsByTagName('meta')\n    assert(\n      metaTagsInDocument.find((metaTag) =>\n        metaTag.getAttribute('charset') === 'utf-8'\n      ),\n    )\n\n    const [bodyElement] = documentElement.getElementsByTagName('body')\n    assert(bodyElement)\n\n    const [headingElementInBody] = [...bodyElement.children]\n    assert(\n      headingElementInBody !== undefined &&\n        headingElementInBody.tagName === 'H1' &&\n        headingElementInBody.textContent === 'Hello world!',\n    )\n  })\n})\n"
  },
  {
    "path": "test/request.test.ts",
    "content": "// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.\nimport { assertEquals } from 'std/assert/mod.ts'\nimport { z } from 'zod'\nimport cheetah from '../mod.ts'\n\nDeno.test('Request', async (t) => {\n  await t.step('req.param()', async () => {\n    const app = new cheetah()\n\n    app.get('/users/:name', (c) => c.req.param('name'))\n    assertEquals(\n      await (await app.fetch(\n        new Request('http://localhost/users/johndoe'),\n      )).text(),\n      'johndoe',\n    )\n  })\n\n  // await t.step('req.ip', async () => {\n  //   const app = new cheetah()\n\n  //   app.get('/ip', (c) => {\n  //     return c.req.ip\n  //   })\n\n  //   const result = await (await app.fetch(new Request('http://localhost/ip')))\n  //     .text()\n  //   assertEquals(result, '127.0.0.1')\n  // })\n\n  await t.step('req.raw()', async () => {\n    const app = new cheetah()\n\n    app.post('/raw', (c) => c.req.raw.method)\n\n    assertEquals(\n      await (await app.fetch(\n        new Request('http://localhost/raw', { method: 'POST' }),\n      )).text(),\n      'POST',\n    )\n  })\n\n  await t.step('req.body()', async (t) => {\n    const app = new cheetah()\n\n    const text = async (pathname: string, body: BodyInit) => {\n      try {\n        return await (await app.fetch(\n          new Request(`http://localhost/${pathname}`, {\n            method: 'POST',\n            body,\n          }),\n        )).text()\n      } catch (_err) {\n        return null\n      }\n    }\n\n    await t.step('req.body() - union (object, object)', async () => {\n      app.post('/b', {\n        body: z.union([\n          z.object({\n            bar: z.literal('foo'),\n          }),\n          z.object({\n            foo: z.literal('bar'),\n          }),\n        ]),\n      }, async (c) => {\n        await c.req.body()\n\n        return 'ok'\n      })\n\n      assertEquals(\n        await text(\n          'b',\n          'test',\n        ),\n        'Bad Request',\n      )\n\n      assertEquals(\n        await text(\n          'b',\n          JSON.stringify({\n            foo: 'bar',\n          }),\n        ),\n        'ok',\n      )\n\n      assertEquals(\n        await text(\n          'b',\n          JSON.stringify({\n            bar: 'foo',\n          }),\n        ),\n        'ok',\n      )\n\n      assertEquals(\n        await text(\n          'b',\n          JSON.stringify({\n            abc: 'def',\n          }),\n        ),\n        'Bad Request',\n      )\n    })\n\n    await t.step('req.body() - union (string, string)', async () => {\n      app.post('/c', {\n        body: z.union([\n          z.string().startsWith('http://'),\n          z.string().startsWith('https://'),\n        ]),\n      }, async (c) => {\n        await c.req.body()\n\n        return 'ok'\n      })\n\n      assertEquals(\n        await text('c', 'foo'),\n        'Bad Request',\n      )\n\n      assertEquals(\n        await text(\n          'c',\n          JSON.stringify({\n            foo: 'bar',\n          }),\n        ),\n        'Bad Request',\n      )\n\n      assertEquals(\n        await text('c', 'https://foo.bar'),\n        'ok',\n      )\n\n      assertEquals(\n        await text('c', 'http://foo.bar'),\n        'ok',\n      )\n    })\n\n    await t.step('req.body() - string', async () => {\n      app.post('/d', {\n        body: z.string().max(3),\n      }, async (c) => {\n        await c.req.body()\n\n        return 'ok'\n      })\n\n      assertEquals(\n        await text('d', 'foo'),\n        'ok',\n      )\n\n      assertEquals(\n        await text('d', 'bar'),\n        'ok',\n      )\n\n      assertEquals(\n        await text(\n          'd',\n          'foo bar',\n        ),\n        'Bad Request',\n      )\n    })\n\n    await t.step('req.body() - object', async () => {\n      app.post('/e', {\n        body: z.object({\n          foo: z.literal('bar'),\n        }),\n      }, async (c) => {\n        await c.req.body()\n\n        return 'ok'\n      })\n\n      assertEquals(\n        await text('e', 'test'),\n        'Bad Request',\n      )\n\n      assertEquals(\n        await text(\n          'e',\n          JSON.stringify({\n            foo: 'bar',\n          }),\n        ),\n        'ok',\n      )\n\n      assertEquals(\n        await text(\n          'e',\n          JSON.stringify({\n            abc: 'def',\n          }),\n        ),\n        'Bad Request',\n      )\n    })\n\n    await t.step('req.body() - record', async () => {\n      app.post('/f', {\n        body: z.record(z.string()),\n      }, async (c) => {\n        await c.req.body()\n\n        return 'ok'\n      })\n\n      assertEquals(\n        await text('f', 'test'),\n        'Bad Request',\n      )\n\n      assertEquals(\n        await text(\n          'f',\n          JSON.stringify({\n            foo: 'bar',\n          }),\n        ),\n        'ok',\n      )\n\n      assertEquals(\n        await text(\n          'f',\n          JSON.stringify({\n            abc: 'def',\n          }),\n        ),\n        'ok',\n      )\n    })\n  })\n\n  await t.step('req.buffer()', async () => {\n    const app = new cheetah()\n\n    app.post(\n      '/buffer',\n      async (c) => await c.req.buffer() instanceof ArrayBuffer ? 'ok' : 'error',\n    )\n\n    assertEquals(\n      await (await app.fetch(\n        new Request('http://localhost/buffer', {\n          method: 'POST',\n          body: 'test',\n        }),\n      )).text(),\n      'ok',\n    )\n  })\n\n  await t.step('req.blob()', async () => {\n    const app = new cheetah()\n\n    app.post(\n      '/blob',\n      async (c) => await c.req.blob() instanceof Blob ? 'ok' : 'error',\n    )\n\n    assertEquals(\n      await (await app.fetch(\n        new Request('http://localhost/blob', {\n          method: 'POST',\n          body: 'test',\n        }),\n      )).text(),\n      'ok',\n    )\n  })\n\n  await t.step('req.formData()', async () => {\n    const app = new cheetah()\n\n    app.post(\n      '/formData',\n      async (c) => await c.req.formData() instanceof FormData ? 'ok' : 'error',\n    )\n    app.post(\n      '/notFormData',\n      async (c) => await c.req.formData() === null ? 'ok' : 'error',\n    )\n\n    assertEquals(\n      await (await app.fetch(\n        new Request('http://localhost/formData', {\n          method: 'POST',\n          body: new FormData(),\n        }),\n      )).text(),\n      'ok',\n    )\n    assertEquals(\n      await (await app.fetch(\n        new Request('http://localhost/notFormData', {\n          method: 'POST',\n          body: 'test',\n        }),\n      )).text(),\n      'ok',\n    )\n  })\n\n  await t.step('req.stream()', async () => {\n    const app = new cheetah()\n\n    app.post('/stream', async (c) => {\n      const stream = c.req.stream\n\n      const text = stream !== null && stream instanceof ReadableStream\n        ? 'ok'\n        : 'error'\n\n      if (stream !== null) {\n        await stream.cancel()\n      }\n\n      return text\n    })\n\n    assertEquals(\n      await (await app.fetch(\n        new Request('http://localhost/stream', {\n          method: 'POST',\n          body: new ReadableStream(),\n        }),\n      )).text(),\n      'ok',\n    )\n  })\n})\n"
  },
  {
    "path": "test/response.test.ts",
    "content": "// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.\nimport { assertEquals } from 'std/assert/mod.ts'\nimport cheetah from '../mod.ts'\n\nDeno.test('Response', async (t) => {\n  const app = new cheetah()\n\n  await t.step('res.code', async () => {\n    app.get('/code-explicit', (c) => {\n      c.res.code = 101\n    })\n    assertEquals(\n      (await app.fetch(new Request('http://localhost/code-explicit'))).status,\n      101,\n    )\n\n    app.get('/code-implicit', () => {\n      return {\n        code: 403,\n      }\n    })\n    assertEquals(\n      (await app.fetch(new Request('http://localhost/code-implicit'))).status,\n      403,\n    )\n  })\n\n  await t.step('res.bodySize', async () => {\n    app.get('/body_size', (c) => {\n      c.res.body = 'hello world'\n\n      return c.res.bodySize.toString()\n    })\n\n    const result = await app.fetch(\n      new Request('http://localhost/body_size'),\n    )\n\n    assertEquals(\n      await result.text(),\n      'hello world'.length.toString(),\n    )\n  })\n\n  await t.step('res.setCookie()', async () => {\n    app.get('/cookie', (c) => c.res.setCookie('custom', 'test'))\n    assertEquals(\n      (await app.fetch(new Request('http://localhost/cookie'))).headers\n        .get('set-cookie'),\n      'custom=test;',\n    )\n  })\n\n  // TODO add test suite for res.deleteCookie()\n\n  await t.step('res.header()', async () => {\n    app.get('/header', (c) => c.res.header('custom', 'test'))\n    assertEquals(\n      (await app.fetch(new Request('http://localhost/header'))).headers\n        .get('custom'),\n      'test',\n    )\n  })\n\n  await t.step('res.redirect()', async () => {\n    // temporary redirect\n    app.get('/temporaryredirect', (c) => c.res.redirect('https://deno.com'))\n\n    const response1 = await app.fetch(\n      new Request('http://localhost/temporaryredirect'),\n    )\n    assertEquals(response1.status, 307)\n    assertEquals(response1.headers.get('location'), 'https://deno.com')\n\n    // permanent redirect\n    app.get(\n      '/permanentredirect',\n      (c) => c.res.redirect('https://deno.com', 301),\n    )\n\n    const response2 = await app.fetch(\n      new Request('http://localhost/permanentredirect'),\n    )\n    assertEquals(response2.status, 301)\n    assertEquals(response2.headers.get('location'), 'https://deno.com')\n  })\n\n  await t.step('blob', async () => {\n    const blob = new Blob([new TextEncoder().encode('test')])\n\n    app.get('/blob', () => {\n      return blob\n    })\n\n    assertEquals(\n      new TextDecoder().decode(\n        await (await (await app.fetch(\n          new Request('http://localhost/blob'),\n        )).blob()).arrayBuffer(),\n      ),\n      'test',\n    )\n  })\n\n  await t.step('stream', async () => {\n    app.get('/stream', () => {\n      return new ReadableStream()\n    })\n\n    assertEquals(\n      (await app.fetch(new Request('http://localhost/stream'))).body !==\n        null,\n      true,\n    )\n  })\n\n  await t.step('formData', async () => {\n    const formData = new FormData()\n    formData.append('one', 'field')\n\n    app.get('/formData', () => {\n      return formData\n    })\n\n    assertEquals(\n      await (await app.fetch(new Request('http://localhost/formData')))\n        .formData(),\n      formData,\n    )\n  })\n\n  await t.step('buffer', async () => {\n    const buffer = new TextEncoder().encode('test')\n    const arrayBuffer = buffer.buffer\n\n    app.get('/buffer', () => {\n      return buffer\n    })\n\n    assertEquals(\n      await (await app.fetch(new Request('http://localhost/buffer')))\n        .arrayBuffer(),\n      arrayBuffer,\n    )\n  })\n\n  await t.step('json', async () => {\n    app.get('/json', () => {\n      return {\n        message: 'test',\n      }\n    })\n\n    assertEquals(\n      await (await app.fetch(\n        new Request('http://localhost/json'),\n      )).json(),\n      { message: 'test' },\n    )\n  })\n\n  await t.step('text', async () => {\n    app.get('/text', () => {\n      return 'test'\n    })\n\n    assertEquals(\n      await (await app.fetch(\n        new Request('http://localhost/text'),\n      )).text(),\n      'test',\n    )\n  })\n})\n"
  },
  {
    "path": "test/routing/versioning.test.ts",
    "content": "import { assertEquals } from 'std/assert/mod.ts'\nimport { cheetah } from '../../cheetah.ts'\n\nDeno.test('versioning', async (t) => {\n  /* uri ---------------------------------------------------------------------- */\n\n  await t.step('uri', async () => {\n    const app = new cheetah({\n      versioning: {\n        current: 'v4', // latest\n        type: 'uri',\n      },\n    })\n\n    async function get(path: string) {\n      const res = await app.fetch(new Request(`http://localhost${path}`))\n\n      return await res.text()\n    }\n\n    // unspecified\n\n    app.get('/1', (c) => `hello from v${c.req.gateway}`)\n\n    assertEquals(await get('/v1/1'), 'hello from v1')\n    assertEquals(await get('/v2/1'), 'hello from v2')\n    assertEquals(await get('/v3/1'), 'hello from v3')\n    assertEquals(await get('/v4/1'), 'hello from v4')\n    assertEquals(await get('/v5/1') !== 'hello from v5', true)\n\n    // exact\n\n    app.get('/2', { gateway: 'v3' }, (c) => `hello from v${c.req.gateway}`)\n\n    assertEquals(await get('/v1/2') !== 'hello from v1', true)\n    assertEquals(await get('/v2/2') !== 'hello from v2', true)\n    assertEquals(await get('/v3/2'), 'hello from v3')\n    assertEquals(await get('/v4/2') !== 'hello from v4', true)\n\n    // smaller than\n\n    app.get('/3', { gateway: '< v3' }, (c) => `hello from v${c.req.gateway}`)\n\n    assertEquals(await get('/v1/3'), 'hello from v1')\n    assertEquals(await get('/v2/3'), 'hello from v2')\n    assertEquals(await get('/v3/3') !== 'hello from v3', true)\n    assertEquals(await get('/v4/3') !== 'hello from v4', true)\n\n    // smaller than or equal to\n\n    app.get('/4', { gateway: '<= v3' }, (c) => `hello from v${c.req.gateway}`)\n\n    assertEquals(await get('/v1/4'), 'hello from v1')\n    assertEquals(await get('/v2/4'), 'hello from v2')\n    assertEquals(await get('/v3/4'), 'hello from v3')\n    assertEquals(await get('/v4/4') !== 'hello from v4', true)\n\n    // greater than\n\n    app.get('/5', { gateway: '> v3' }, (c) => `hello from v${c.req.gateway}`)\n\n    assertEquals(await get('/v1/5') !== 'hello from v1', true)\n    assertEquals(await get('/v2/5') !== 'hello from v2', true)\n    assertEquals(await get('/v3/5') !== 'hello from v3', true)\n    assertEquals(await get('/v4/5'), 'hello from v4')\n\n    // greater than or equal\n\n    app.get('/6', { gateway: '>= v3' }, (c) => `hello from v${c.req.gateway}`)\n\n    assertEquals(await get('/v1/6') !== 'hello from v1', true)\n    assertEquals(await get('/v2/6') !== 'hello from v2', true)\n    assertEquals(await get('/v3/6'), 'hello from v3')\n    assertEquals(await get('/v4/6'), 'hello from v4')\n\n    // from (min) ... to (max)\n\n    app.get(\n      '/7',\n      { gateway: 'v2...v3' },\n      (c) => `hello from v${c.req.gateway}`,\n    )\n\n    assertEquals(await get('/v1/7') !== 'hello from v1', true)\n    assertEquals(await get('/v2/7'), 'hello from v2')\n    assertEquals(await get('/v3/7'), 'hello from v3')\n    assertEquals(await get('/v4/7') !== 'hello from v4', true)\n\n    // with base path\n    const app2 = new cheetah({\n      base: '/api',\n      versioning: {\n        current: 'v4', // latest\n        type: 'uri',\n      },\n    })\n\n    async function get2(path: string) {\n      const res = await app2.fetch(new Request(`http://localhost${path}`))\n\n      return await res.text()\n    }\n\n    app2.get(\n      '/7',\n      { gateway: 'v2...v3' },\n      (c) => `hello from v${c.req.gateway}`,\n    )\n\n    assertEquals(await get2('/api/v1/7') !== 'hello from v1', true)\n    assertEquals(await get2('/api/v2/7'), 'hello from v2')\n    assertEquals(await get2('/api/v3/7'), 'hello from v3')\n    assertEquals(await get2('/api/v4/7') !== 'hello from v4', true)\n  })\n\n  /* header ------------------------------------------------------------------- */\n\n  await t.step('header', async () => {\n    const app = new cheetah({\n      versioning: {\n        current: 'v4', // latest\n        type: 'header',\n        header: 'x-version',\n      },\n    })\n\n    async function get(path: string) {\n      path = path.replace('/', '')\n\n      const res = await app.fetch(\n        new Request(`http://localhost/${path.split('/')[1]}`, {\n          headers: {\n            'x-version': `${path.split('/')[0]}`,\n          },\n        }),\n      )\n\n      return await res.text()\n    }\n\n    // unspecified\n\n    app.get('/1', (c) => `hello from v${c.req.gateway}`)\n\n    assertEquals(await get('/v1/1'), 'hello from v1')\n    assertEquals(await get('/v2/1'), 'hello from v2')\n    assertEquals(await get('/v3/1'), 'hello from v3')\n    assertEquals(await get('/v4/1'), 'hello from v4')\n    assertEquals(await get('/v5/1') !== 'hello from v5', true)\n\n    // exact\n\n    app.get('/2', { gateway: 'v3' }, (c) => `hello from v${c.req.gateway}`)\n\n    assertEquals(await get('/v1/2') !== 'hello from v1', true)\n    assertEquals(await get('/v2/2') !== 'hello from v2', true)\n    assertEquals(await get('/v3/2'), 'hello from v3')\n    assertEquals(await get('/v4/2') !== 'hello from v4', true)\n\n    // smaller than\n\n    app.get('/3', { gateway: '< v3' }, (c) => `hello from v${c.req.gateway}`)\n\n    assertEquals(await get('/v1/3'), 'hello from v1')\n    assertEquals(await get('/v2/3'), 'hello from v2')\n    assertEquals(await get('/v3/3') !== 'hello from v3', true)\n    assertEquals(await get('/v4/3') !== 'hello from v4', true)\n\n    // smaller than or equal to\n\n    app.get('/4', { gateway: '<= v3' }, (c) => `hello from v${c.req.gateway}`)\n\n    assertEquals(await get('/v1/4'), 'hello from v1')\n    assertEquals(await get('/v2/4'), 'hello from v2')\n    assertEquals(await get('/v3/4'), 'hello from v3')\n    assertEquals(await get('/v4/4') !== 'hello from v4', true)\n\n    // greater than\n\n    app.get('/5', { gateway: '> v3' }, (c) => `hello from v${c.req.gateway}`)\n\n    assertEquals(await get('/v1/5') !== 'hello from v1', true)\n    assertEquals(await get('/v2/5') !== 'hello from v2', true)\n    assertEquals(await get('/v3/5') !== 'hello from v3', true)\n    assertEquals(await get('/v4/5'), 'hello from v4')\n\n    // greater than or equal\n\n    app.get('/6', { gateway: '>= v3' }, (c) => `hello from v${c.req.gateway}`)\n\n    assertEquals(await get('/v1/6') !== 'hello from v1', true)\n    assertEquals(await get('/v2/6') !== 'hello from v2', true)\n    assertEquals(await get('/v3/6'), 'hello from v3')\n    assertEquals(await get('/v4/6'), 'hello from v4')\n\n    // from (min) ... to (max)\n\n    app.get(\n      '/7',\n      { gateway: 'v2...v3' },\n      (c) => `hello from v${c.req.gateway}`,\n    )\n\n    assertEquals(await get('/v1/7') !== 'hello from v1', true)\n    assertEquals(await get('/v2/7'), 'hello from v2')\n    assertEquals(await get('/v3/7'), 'hello from v3')\n    assertEquals(await get('/v4/7') !== 'hello from v4', true)\n  })\n})\n"
  },
  {
    "path": "test/routing.test.ts",
    "content": "// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.\nimport { assertEquals } from 'std/assert/mod.ts'\nimport cheetah, { Collection } from '../mod.ts'\n\nDeno.test('Nesting/Routing', async (t) => {\n  const collection = new Collection()\n    .get('/', () => 'nested root route')\n    .get('/nestedroute', () => 'nested route')\n\n  await t.step('Root with Base Path', async () => {\n    const r1 = new cheetah({\n      base: '/api',\n    })\n      .use('/collection', collection)\n\n    assertEquals(\n      await (await r1.fetch(\n        new Request('http://localhost/api/collection'),\n      )).text(),\n      'nested root route',\n    )\n    assertEquals(\n      await (await r1.fetch(\n        new Request('http://localhost/api/collection/nestedroute'),\n      )).text(),\n      'nested route',\n    )\n  })\n\n  await t.step('Root without Base Path', async () => {\n    const r2 = new cheetah()\n      .use('/collection', collection)\n\n    assertEquals(\n      await (await r2.fetch(new Request('http://localhost/collection')))\n        .text(),\n      'nested root route',\n    )\n    assertEquals(\n      await (await r2.fetch(\n        new Request('http://localhost/collection/nestedroute'),\n      )).text(),\n      'nested route',\n    )\n  })\n})\n"
  },
  {
    "path": "test/validation.test.ts",
    "content": "// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.\nimport { assertEquals } from 'std/assert/mod.ts'\nimport { z } from 'zod'\nimport cheetah from '../mod.ts'\n\nDeno.test('Validation', async (t) => {\n  await t.step('transform', async () => {\n    const app = new cheetah()\n\n    app.post('/transform', {\n      body: z.object({\n        message: z.string(),\n      }),\n    }, async (c) => {\n      assertEquals(await c.req.body({ transform: true }), {\n        message: 'Hello World',\n      })\n\n      return 'test'\n    })\n\n    const form = new FormData()\n    form.append('message', 'Hello World')\n    await (await app.fetch(\n      new Request('http://localhost/transform', {\n        method: 'POST',\n        body: form,\n      }),\n    )).text()\n\n    await (await app.fetch(\n      new Request('http://localhost/transform', {\n        method: 'POST',\n        body: JSON.stringify({ message: 'Hello World' }),\n      }),\n    )).text()\n  })\n\n  await t.step('cookies', async () => {\n    const app = new cheetah()\n\n    app.get('/cookies', {\n      cookies: z.object({ custom: z.string().min(4).max(16) }).strict(),\n    }, (c) => {\n      return c.req.cookies\n    })\n\n    assertEquals(\n      (await app.fetch(\n        new Request('http://localhost/cookies', {\n          headers: { cookies: 'custom=test;' },\n        }),\n      )).status,\n      200,\n    )\n    assertEquals(\n      (await app.fetch(\n        new Request('http://localhost/cookies', {\n          headers: { cookie: 'custom=te;' },\n        }),\n      )).status,\n      400,\n    )\n    assertEquals(\n      (await app.fetch(\n        new Request('http://localhost/cookies', {\n          headers: { cookie: 'invalid=abc;' },\n        }),\n      )).status,\n      400,\n    )\n    assertEquals(\n      (await app.fetch(\n        new Request('http://localhost/cookies', {\n          headers: { cookie: 'custom=test; another=cookie;' },\n        }),\n      )).status,\n      400,\n    )\n  })\n\n  await t.step('headers', async () => {\n    const app = new cheetah()\n\n    app.get(\n      '/headers',\n      { headers: z.object({ custom: z.string().email() }) },\n      (c) => {\n        return c.req.headers\n      },\n    )\n\n    const res1 = await app.fetch(\n      new Request('http://localhost/headers', {\n        headers: { random: 'bullshit' },\n      }),\n    )\n    const res2 = await app.fetch(\n      new Request('http://localhost/headers', {\n        headers: { random: 'bullshit', custom: 'tes@t' },\n      }),\n    )\n    const res3 = await app.fetch(\n      new Request('http://localhost/headers', {\n        headers: { custom: '' },\n      }),\n    )\n    const res4 = await app.fetch(\n      new Request('http://localhost/headers', {\n        headers: { custom: 'test@email.com' },\n      }),\n    )\n\n    assertEquals(res1.status, 400)\n    assertEquals(res2.status, 400)\n    assertEquals(res3.status, 400)\n    assertEquals(res4.status, 200)\n    assertEquals(await res4.json(), { custom: 'test@email.com' })\n  })\n\n  await t.step('query', async () => {\n    const app = new cheetah()\n\n    app.get('/query', {\n      query: z.object({\n        first: z.string().optional(),\n        second: z.boolean(),\n        third: z.number(),\n      }),\n    }, (c) => {\n      const d = c.req.query\n\n      d.first\n\n      return c.req.query\n    })\n\n    assertEquals(\n      (await app.fetch(\n        new Request('http://localhost/query?first=test&second&third=69'),\n      )).status,\n      200,\n    )\n    assertEquals(\n      (await app.fetch(\n        new Request('http://localhost/query?second=false&third=e9'),\n      )).status,\n      400,\n    )\n    assertEquals(\n      (await app.fetch(\n        new Request('http://localhost/query?second=false&third=69'),\n      )).status,\n      200,\n    )\n  })\n\n  await t.step('query (no schema)', async () => {\n    const app = new cheetah()\n\n    app.get('/query', (c) => {\n      const d = c.req.query\n\n      d.lol\n      return d\n    })\n\n    assertEquals(\n      await (await app.fetch(\n        new Request('http://localhost/query?first=test&second&third=69'),\n      )).json(),\n      { first: 'test', second: true, third: 69 },\n    )\n    assertEquals(\n      await (await app.fetch(\n        new Request('http://localhost/query?second=false&third=e9'),\n      )).json(),\n      { second: false, third: 'e9' },\n    )\n    assertEquals(\n      await (await app.fetch(\n        new Request('http://localhost/query?second=false&third=69'),\n      )).json(),\n      { second: false, third: 69 },\n    )\n  })\n\n  await t.step('params', async () => {\n    const app = new cheetah()\n\n    app.get(\n      '/animals/:name',\n      {\n        params: {\n          name: z.union([z.literal('cat'), z.literal('dog')]),\n        },\n      },\n      (c) => {\n        return c.req.param('name')\n      },\n    )\n\n    const res1 = await app.fetch(\n      new Request('http://localhost/animals/cat'),\n    )\n\n    assertEquals(res1.status, 200)\n\n    const res2 = await app.fetch(\n      new Request('http://localhost/animals/dog'),\n    )\n\n    assertEquals(res2.status, 200)\n\n    const res3 = await app.fetch(\n      new Request('http://localhost/animals/rabbit'),\n    )\n\n    assertEquals(res3.status, 400)\n  })\n})\n"
  }
]