├── .eslintignore ├── .env.example ├── .prettierrc ├── migrations ├── 0002_add_bundle_version.sql ├── 0000_create_requests_table.sql └── 0001_make_service_id_a_TEXT_instead_of_INTEGER.sql ├── jest.config.ts ├── .editorconfig ├── src ├── middleware │ ├── analytics.ts │ └── validate.ts ├── d1 │ ├── models.ts │ └── index.ts ├── crons │ ├── cron.ts │ └── delete_old_data_cron.ts ├── bindings.ts ├── types.ts ├── header_utils.ts ├── utils.ts └── index.ts ├── .eslintrc.js ├── .github └── workflows │ ├── test.yml │ └── deploy.yml ├── package.json ├── wrangler.toml ├── tests ├── utils.ts ├── shodan.test.ts ├── domain-availability.whoisxmlapi.test.ts ├── compute_renderer.test.ts ├── crons.test.ts ├── db.log.test.ts ├── open_ai.test.ts ├── index.test.ts └── cloudflare.ai.gateway.test.ts ├── .gitignore ├── README.md └── tsconfig.json /.eslintignore: -------------------------------------------------------------------------------- 1 | .eslintrc.js 2 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | API_OPENAI_COM_API_KEY= 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 140, 3 | "singleQuote": false, 4 | "useTabs": false, 5 | "tabWidth": 2, 6 | "semi": true 7 | } 8 | -------------------------------------------------------------------------------- /migrations/0002_add_bundle_version.sql: -------------------------------------------------------------------------------- 1 | -- Migration number: 0002 2024-01-03T01:58:44.126Z 2 | 3 | ALTER TABLE requests ADD COLUMN bundle_version TEXT; 4 | -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | const config = { 2 | testEnvironment: "miniflare", 3 | testMatch: ["**/tests/**/(*.)+(spec|test).+(ts|tsx)"], 4 | transform: { 5 | "^.+\\.(ts|tsx)$": "esbuild-jest", 6 | }, 7 | }; 8 | 9 | export default config; 10 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | tab_width = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.yml] 13 | indent_style = space 14 | -------------------------------------------------------------------------------- /src/middleware/analytics.ts: -------------------------------------------------------------------------------- 1 | import { MiddlewareHandler } from "hono"; 2 | import D1 from "../d1"; 3 | 4 | function analytics(): MiddlewareHandler { 5 | return async (context, next) => { 6 | await next(); 7 | // Asynchronously save analytics params 8 | context.executionCtx.waitUntil(new D1(context).saveAnalyticsParams()); 9 | }; 10 | } 11 | 12 | export default analytics; 13 | -------------------------------------------------------------------------------- /src/d1/models.ts: -------------------------------------------------------------------------------- 1 | // RequestModel is the type of the database table called "requests". 2 | export interface RequestModel { 3 | id: number; 4 | user_agent: string; 5 | cf_connecting_ip: string; 6 | cf_ip_country: string; 7 | service_id: string; 8 | service_name: string; 9 | identifier_for_vendor: string; 10 | bundle_identifier: string; 11 | url: string; 12 | headers: string; 13 | status_code: number; 14 | error: string; 15 | bundle_version: string; 16 | } 17 | -------------------------------------------------------------------------------- /src/crons/cron.ts: -------------------------------------------------------------------------------- 1 | import { Bindings } from "../bindings"; 2 | import { ExecutionContext } from "hono/dist/types/context"; 3 | import delete_old_data_cron from "./delete_old_data_cron"; 4 | 5 | async function cron(event: ScheduledEvent, env: Bindings, context: ExecutionContext) { 6 | switch (event.cron) { 7 | case env.DELETE_OLD_DATA_CRON: 8 | await delete_old_data_cron(env); 9 | break; 10 | default: 11 | context.passThroughOnException(); 12 | console.warn(`Unknown cron job: ${event.cron}`); 13 | } 14 | } 15 | 16 | export default cron; 17 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es2021: true, 5 | }, 6 | extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended"], 7 | overrides: [], 8 | parser: "@typescript-eslint/parser", 9 | parserOptions: { 10 | ecmaVersion: "latest", 11 | sourceType: "module", 12 | }, 13 | plugins: ["@typescript-eslint"], 14 | rules: { 15 | indent: ["error", 2, { SwitchCase: 1 }], 16 | "linebreak-style": ["error", "unix"], 17 | quotes: ["error", "double"], 18 | semi: ["error", "always"], 19 | }, 20 | }; 21 | -------------------------------------------------------------------------------- /src/crons/delete_old_data_cron.ts: -------------------------------------------------------------------------------- 1 | import { Bindings } from "../bindings"; 2 | 3 | async function delete_old_data_cron(env: Bindings) { 4 | const response = await env.DB.prepare( 5 | ` 6 | DELETE 7 | FROM requests 8 | WHERE created_at < datetime('now', ?1) 9 | `, 10 | ) 11 | .bind(env.DELETE_OLD_DATA_BEFORE) 12 | .run(); 13 | 14 | if (response.error) { 15 | throw `Delete old data cron failed: ${response.error}`; 16 | } 17 | 18 | console.info(`Delete old data cron completed successfully.\nMetadata: ${JSON.stringify(response.meta)}`); 19 | return response.meta; 20 | } 21 | 22 | export default delete_old_data_cron; 23 | -------------------------------------------------------------------------------- /migrations/0000_create_requests_table.sql: -------------------------------------------------------------------------------- 1 | -- Migration number: 0000 2023-12-20T02:07:03.645Z 2 | 3 | DROP TABLE IF EXISTS requests; 4 | 5 | CREATE TABLE requests 6 | ( 7 | id SERIAL PRIMARY KEY, 8 | user_agent TEXT, 9 | cf_connecting_ip TEXT, 10 | cf_ip_country TEXT, 11 | service_id INTEGER, 12 | service_name TEXT, 13 | identifier_for_vendor TEXT, 14 | bundle_identifier TEXT, 15 | url TEXT, 16 | headers JSON, 17 | status_code INTEGER, 18 | created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP 19 | ); 20 | 21 | -------------------------------------------------------------------------------- /src/bindings.ts: -------------------------------------------------------------------------------- 1 | import { Context } from "hono"; 2 | 3 | /** 4 | * Bindings are the environment variables that are set by the Cloudflare Worker. 5 | */ 6 | export type Bindings = { 7 | [key: string]: string | null | undefined; 8 | } & { 9 | DB: D1Database; 10 | DELETE_OLD_DATA_BEFORE: string; 11 | DELETE_OLD_DATA_CRON: string; 12 | }; 13 | 14 | /** 15 | * Variables are the environment variables that are set by one of the middleware functions. 16 | * They are available in the Hono context throughout the request lifecycle. 17 | */ 18 | export type Variables = { 19 | token: string; 20 | }; 21 | 22 | export type AppContext = Context<{ Bindings: Bindings; Variables: Variables }>; 23 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Test/Lint 5 | 6 | on: 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | name: Test/Lint 15 | strategy: 16 | matrix: 17 | node-version: [16, 18] 18 | steps: 19 | - uses: actions/checkout@v4 20 | - name: Use Node.js ${{ matrix.node-version }} 21 | uses: actions/setup-node@v4 22 | with: 23 | node-version: ${{ matrix.node-version }} 24 | - run: npm ci 25 | - run: npm run test 26 | - run: npm run lint 27 | env: 28 | CI: true -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | export enum ServiceAuthType { 2 | HEADER = "HEADER", 3 | QUERY = "QUERY", 4 | } 5 | 6 | export type TError = { 7 | error: string; 8 | }; 9 | 10 | export type D1ResultMeta = { 11 | served_by: string; 12 | duration: number; 13 | changes: number; 14 | last_row_id: number; 15 | changed_db: boolean; 16 | size_after: number; 17 | }; 18 | 19 | export class ServiceType { 20 | static readonly DIRECT = "DIRECT"; 21 | static readonly GATEWAY = "GATEWAY"; 22 | 23 | static parse(value: string | null): TServiceType { 24 | if (value === null) { 25 | return ServiceType.DIRECT; 26 | } 27 | 28 | switch (value) { 29 | case ServiceType.DIRECT: 30 | case ServiceType.GATEWAY: 31 | return value as TServiceType; 32 | default: 33 | throw new Error("Invalid service type"); 34 | } 35 | } 36 | } 37 | 38 | export type TServiceType = typeof ServiceType.DIRECT | typeof ServiceType.GATEWAY; 39 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | name: Test/Lint 12 | strategy: 13 | matrix: 14 | node-version: [16, 18] 15 | steps: 16 | - uses: actions/checkout@v4 17 | - name: Use Node.js ${{ matrix.node-version }} 18 | uses: actions/setup-node@v4 19 | with: 20 | node-version: ${{ matrix.node-version }} 21 | - run: npm ci 22 | - run: npm run test 23 | - run: npm run lint 24 | env: 25 | CI: true 26 | deploy: 27 | runs-on: ubuntu-latest 28 | name: Deploy 29 | needs: build 30 | steps: 31 | - uses: actions/checkout@v3 32 | - name: Deploy 33 | uses: cloudflare/wrangler-action@v3 34 | with: 35 | apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} 36 | preCommands: wrangler d1 migrations apply gateway --env production 37 | command: deploy --env production 38 | -------------------------------------------------------------------------------- /src/header_utils.ts: -------------------------------------------------------------------------------- 1 | class HeaderUtils { 2 | private readonly headers: Headers; 3 | constructor(headers: Headers) { 4 | this.headers = new Headers(headers); 5 | } 6 | 7 | /** 8 | * A function to remove sensitive headers from a Headers object. 9 | */ 10 | removeSensitiveHeaders() { 11 | const sensitiveHeaders = ["Authorization", "x-gateway-service-token"]; 12 | 13 | for (const header of sensitiveHeaders) { 14 | this.headers.delete(header); 15 | } 16 | 17 | return this; 18 | } 19 | 20 | /** 21 | * A function to remove all x-gateway-* headers from a Headers object. 22 | */ 23 | removeGatewayHeaders() { 24 | for (const [key] of this.headers) { 25 | if (key.startsWith("x-gateway-")) { 26 | this.headers.delete(key); 27 | } 28 | } 29 | 30 | return this; 31 | } 32 | 33 | get() { 34 | return this.headers; 35 | } 36 | 37 | toJsonObject() { 38 | return Object.fromEntries(this.headers.entries()); 39 | } 40 | 41 | toJsonString() { 42 | return JSON.stringify(this.toJsonObject()); 43 | } 44 | } 45 | 46 | export default HeaderUtils; 47 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gateway", 3 | "version": "0.0.0", 4 | "private": true, 5 | "overrides": { 6 | "@types/node": "20.8.3" 7 | }, 8 | "scripts": { 9 | "deploy": "wrangler deploy", 10 | "dev": "wrangler dev", 11 | "dev:scheduled": "wrangler dev --test-scheduled", 12 | "start": "wrangler dev", 13 | "lint": "eslint --ext .js,.ts,.tsx ./src --fix", 14 | "test": "jest" 15 | }, 16 | "devDependencies": { 17 | "@cloudflare/workers-types": "^4.20230419.0", 18 | "@types/jest": "^29.5.8", 19 | "@typescript-eslint/eslint-plugin": "^6.11.0", 20 | "better-sqlite3": "^8.0.1", 21 | "esbuild": "^0.19.8", 22 | "esbuild-jest": "^0.5.0", 23 | "eslint": "^8.53.0", 24 | "eslint-config-standard-with-typescript": "^39.1.1", 25 | "eslint-plugin-import": "^2.29.0", 26 | "eslint-plugin-n": "^16.3.1", 27 | "eslint-plugin-promise": "^6.1.1", 28 | "jest": "^29.7.0", 29 | "jest-environment-miniflare": "^2.14.1", 30 | "ts-node": "^10.9.2", 31 | "typescript": "^5.2.2", 32 | "wrangler": "^3.22.1" 33 | }, 34 | "dependencies": { 35 | "hono": "^3.11.9" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /wrangler.toml: -------------------------------------------------------------------------------- 1 | name = "gateway" 2 | main = "src/index.ts" 3 | compatibility_date = "2023-10-30" 4 | 5 | ############################# LOCAL ################################# 6 | 7 | [triggers] 8 | crons = ["0 0 * * SUN"] # At 12:00 AM, every sunday 9 | 10 | [[d1_databases]] 11 | binding = "DB" 12 | database_name = "gateway_local" 13 | database_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" 14 | migrations_table = "migrations" 15 | migrations_dir = "migrations" 16 | 17 | [vars] 18 | DELETE_OLD_DATA_BEFORE = "-5 minutes" # Standard SQL format 19 | DELETE_OLD_DATA_CRON = "0 0 * * SUN" # check crons under triggers section 20 | 21 | ############################# PRODUCTION ############################# 22 | 23 | [env.production] 24 | name = "gateway" 25 | 26 | [env.production.triggers] 27 | crons = ["0 0 * * SUN"] # At 12:00 AM, every sunday 28 | 29 | [[env.production.d1_databases]] 30 | binding = "DB" 31 | database_name = "gateway" 32 | database_id = "bea25784-0c7b-46d3-a064-47b6ccafa92e" 33 | migrations_table = "migrations" 34 | migrations_dir = "migrations" 35 | 36 | [env.production.vars] 37 | DELETE_OLD_DATA_BEFORE = "-3 months" # Standard SQL format 38 | DELETE_OLD_DATA_CRON = "0 0 * * SUN" # check crons under env.production.triggers section 39 | -------------------------------------------------------------------------------- /migrations/0001_make_service_id_a_TEXT_instead_of_INTEGER.sql: -------------------------------------------------------------------------------- 1 | -- Migration number: 0001 2023-12-27T04:07:34.039Z 2 | 3 | 4 | CREATE TABLE temp_requests 5 | ( 6 | id INTEGER PRIMARY KEY AUTOINCREMENT, 7 | user_agent TEXT, 8 | cf_connecting_ip TEXT, 9 | cf_ip_country TEXT, 10 | service_id TEXT, 11 | service_name TEXT, 12 | identifier_for_vendor TEXT, 13 | bundle_identifier TEXT, 14 | url TEXT, 15 | headers JSON, 16 | status_code INTEGER, 17 | error TEXT, 18 | created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP 19 | ); 20 | 21 | INSERT INTO temp_requests 22 | (user_agent, 23 | cf_connecting_ip, 24 | cf_ip_country, 25 | service_id, 26 | service_name, 27 | identifier_for_vendor, 28 | bundle_identifier, 29 | url, 30 | headers, 31 | status_code) 32 | SELECT user_agent, 33 | cf_connecting_ip, 34 | cf_ip_country, 35 | CAST(service_id as TEXT), 36 | service_name, 37 | identifier_for_vendor, 38 | bundle_identifier, 39 | url, 40 | headers, 41 | status_code 42 | FROM requests; 43 | 44 | DROP TABLE requests; 45 | 46 | ALTER TABLE temp_requests 47 | RENAME TO requests; 48 | 49 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import { TError } from "./types"; 2 | 3 | /** 4 | * A function to stream response 5 | * 6 | * @param body - The response body to be streamed. 7 | * 8 | * @returns A response with the streamed body. 9 | */ 10 | export async function streamResponse(body: ReadableStream | null) { 11 | if (!body) { 12 | return Response.json({ error: "No body to stream!" }, { status: 500 }); 13 | } 14 | 15 | const { readable, writable } = new TransformStream(); 16 | await body.pipeTo(writable); 17 | 18 | return new Response(readable, { 19 | headers: { 20 | "Content-Type": "application/json", 21 | "Transfer-Encoding": "chunked", 22 | }, 23 | }); 24 | } 25 | 26 | /** 27 | * check if the value is not null or undefined in typesafe way. 28 | * @param value - The value to check 29 | * @returns the value. 30 | */ 31 | export function isNotNullOrUndefined(value: T | null | undefined): value is T { 32 | return value !== null && value !== undefined; 33 | } 34 | 35 | /** 36 | * A function to convert a string to an environment variable key. 37 | * 38 | * @param str - The string to convert to an environment variable key. 39 | * @param suffix - The suffixes to add 40 | * 41 | * @returns The environment variable key. 42 | */ 43 | export function toEnvKey(str: string, ...suffix: (string | null | undefined)[]) { 44 | const s = `${str.replace(/[^a-zA-Z0-9]/g, "_")}_API_KEY`; 45 | 46 | return suffix 47 | .filter(isNotNullOrUndefined) 48 | .reduce((a, c) => a + `_${c.replace(/[^a-zA-Z0-9]/g, "_")}`, s) 49 | .toUpperCase(); 50 | } 51 | -------------------------------------------------------------------------------- /tests/utils.ts: -------------------------------------------------------------------------------- 1 | import Database from "better-sqlite3"; 2 | import { D1Database, D1DatabaseAPI } from "@miniflare/d1"; 3 | import * as fs from "fs"; 4 | 5 | export const BINDINGS = getMiniflareBindings(); 6 | 7 | export function getMockOpenAI() { 8 | const fetchMock = getMiniflareFetchMock(); 9 | fetchMock.disableNetConnect(); 10 | 11 | return fetchMock.get("http://api.openai.com"); 12 | } 13 | 14 | export function getMockShodan() { 15 | const fetchMock = getMiniflareFetchMock(); 16 | fetchMock.disableNetConnect(); 17 | 18 | return fetchMock.get("http://api.shodan.io"); 19 | } 20 | 21 | export function getMockComputeRenderer() { 22 | const fetchMock = getMiniflareFetchMock(); 23 | fetchMock.disableNetConnect(); 24 | 25 | return fetchMock.get("http://api.computerender.com"); 26 | } 27 | 28 | export function getMockDomainAvailabilityWhoIsXmlApi() { 29 | const fetchMock = getMiniflareFetchMock(); 30 | fetchMock.disableNetConnect(); 31 | 32 | return fetchMock.get("http://domain-availability.whoisxmlapi.com"); 33 | } 34 | 35 | export function getMockCloudflareAIGateway() { 36 | const fetchMock = getMiniflareFetchMock(); 37 | fetchMock.disableNetConnect(); 38 | 39 | return fetchMock.get("http://gateway.ai.cloudflare.com"); 40 | } 41 | 42 | export async function setInMemoryD1Database() { 43 | const db = new Database(":memory:"); 44 | const migrations = fs.opendirSync("./migrations"); 45 | const files = []; 46 | 47 | for await (const file of migrations) { 48 | files.push(file); 49 | } 50 | 51 | // sort files under migrations directory by name 52 | files.sort((a, b) => a.name.localeCompare(b.name)); 53 | 54 | // run each migration 55 | for (const file of files) { 56 | console.log(`Running migration: ${file.name}`); 57 | db.exec(fs.readFileSync(`./migrations/${file.name}`, "utf-8")); 58 | } 59 | 60 | return new D1Database(new D1DatabaseAPI(db)); 61 | } 62 | -------------------------------------------------------------------------------- /tests/shodan.test.ts: -------------------------------------------------------------------------------- 1 | import { app } from "../src"; 2 | import { BINDINGS, getMockShodan, setInMemoryD1Database } from "./utils"; 3 | import { RequestModel } from "../src/d1/models"; 4 | 5 | describe("it should test all the proxied endpoints for shodan", () => { 6 | // Create an in-memory sqlite database 7 | beforeAll(async () => { 8 | BINDINGS["DB"] = await setInMemoryD1Database(); 9 | BINDINGS["API_SHODAN_IO_API_KEY"] = "shodan-api-key"; 10 | }); 11 | 12 | // Intercept the request to be proxied to api.shodan.io 13 | beforeEach(() => { 14 | getMockShodan() 15 | .intercept({ 16 | method: "GET", 17 | path: "/shodan/host/1.1.1.1", 18 | query: { 19 | key: BINDINGS["API_SHODAN_IO_API_KEY"], 20 | }, 21 | }) 22 | .reply(200); 23 | }); 24 | 25 | it("should return 200 for request proxied to api.shodan.io", async () => { 26 | const response = await app.request( 27 | "/shodan/host/1.1.1.1", 28 | { 29 | method: "GET", 30 | headers: { 31 | "x-gateway-service-host": "api.shodan.io", 32 | "x-gateway-service-auth-type": "QUERY", 33 | "x-gateway-service-auth-key": "key", 34 | "content-type": "application/json", 35 | "x-gateway-identifier-for-vendor": "ccc-bbb-aaa", 36 | "x-gateway-bundle-version": "2.0.0", 37 | }, 38 | }, 39 | BINDINGS, 40 | new ExecutionContext(), 41 | ); 42 | 43 | expect(response.status).toBe(200); 44 | 45 | const { DB } = BINDINGS; 46 | const request: RequestModel = await DB.prepare( 47 | ` 48 | SELECT * 49 | FROM requests 50 | WHERE identifier_for_vendor = ?1 51 | `, 52 | ) 53 | .bind("ccc-bbb-aaa") 54 | .first(); 55 | 56 | expect(request.identifier_for_vendor).toBe("ccc-bbb-aaa"); 57 | expect(request.status_code).toBe(200); 58 | expect(request.bundle_version).toBe("2.0.0"); 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /src/middleware/validate.ts: -------------------------------------------------------------------------------- 1 | import { MiddlewareHandler } from "hono"; 2 | import { ServiceType, TError } from "../types"; 3 | import { toEnvKey } from "../utils"; 4 | import { AppContext } from "../bindings"; 5 | 6 | function validate(): MiddlewareHandler { 7 | return async (context: AppContext, next) => { 8 | const headers = context.req.raw.headers; 9 | const xGatewayServiceHost = headers.get("x-gateway-service-host"); 10 | const xGatewayServiceType = ServiceType.parse(headers.get("x-gateway-service-type")); 11 | const xGatewayServiceProxy = headers.get("x-gateway-service-proxy"); 12 | const xGatewayServiceToken = headers.get("x-gateway-service-token"); 13 | 14 | if (!xGatewayServiceHost) { 15 | return context.json({ error: "x-gateway-service-host header is required." }, 400); 16 | } 17 | 18 | if (xGatewayServiceType === "DIRECT" && xGatewayServiceProxy) { 19 | return context.json({ error: "x-gateway-service-proxy header is not allowed for direct service type." }, 400); 20 | } 21 | 22 | if (xGatewayServiceType === "GATEWAY" && !xGatewayServiceProxy) { 23 | return context.json({ error: "x-gateway-service-proxy header is required." }, 400); 24 | } 25 | 26 | // If the service token is not provided in the request headers, try to get it from the environment variables. 27 | // The environment variable name is the service host name in uppercase with all non-alphanumeric characters replaced with "_". 28 | const apiKey = toEnvKey(xGatewayServiceHost, xGatewayServiceProxy); 29 | const token = xGatewayServiceToken || context.env[apiKey]; 30 | 31 | if (!token) { 32 | return context.json( 33 | { 34 | error: "Cannot find API key for proxied service! Either provide it in the request headers or set it as an environment variable.", 35 | }, 36 | 400, 37 | ); 38 | } 39 | 40 | context.set("token", token); 41 | 42 | return next(); 43 | }; 44 | } 45 | 46 | export default validate; 47 | -------------------------------------------------------------------------------- /tests/domain-availability.whoisxmlapi.test.ts: -------------------------------------------------------------------------------- 1 | import { app } from "../src"; 2 | import { BINDINGS, getMockDomainAvailabilityWhoIsXmlApi, setInMemoryD1Database } from "./utils"; 3 | import { RequestModel } from "../src/d1/models"; 4 | 5 | describe("it should test all the proxied endpoints for domain-availability.whoisxmlapi", () => { 6 | // Create an in-memory sqlite database 7 | beforeAll(async () => { 8 | BINDINGS["DB"] = await setInMemoryD1Database(); 9 | BINDINGS["DOMAIN_AVAILABILITY_WHOISXMLAPI_COM_API_KEY"] = "domain-availability-whoisxmlapi-api-key"; 10 | }); 11 | 12 | // Intercept the request to the proxied service when the API key is provided in the query params 13 | beforeEach(() => { 14 | getMockDomainAvailabilityWhoIsXmlApi() 15 | .intercept({ 16 | method: "GET", 17 | path: "/api/v1", 18 | query: { 19 | apiKey: BINDINGS["DOMAIN_AVAILABILITY_WHOISXMLAPI_COM_API_KEY"], 20 | }, 21 | }) 22 | .reply(200); 23 | }); 24 | 25 | it("should set the API key in QUERY param if x-gateway-service-auth-type is set to QUERY", async () => { 26 | const response = await app.request( 27 | "/api/v1", 28 | { 29 | method: "GET", 30 | headers: { 31 | "x-gateway-service-host": "domain-availability.whoisxmlapi.com", 32 | "x-gateway-service-auth-type": "QUERY", 33 | "x-gateway-service-auth-key": "apiKey", 34 | "content-type": "application/json", 35 | "x-gateway-identifier-for-vendor": "zzz-yyy-xxx", 36 | }, 37 | }, 38 | BINDINGS, 39 | new ExecutionContext(), 40 | ); 41 | 42 | expect(response.status).toBe(200); 43 | 44 | const { DB } = BINDINGS; 45 | const request: RequestModel = await DB.prepare( 46 | ` 47 | SELECT * 48 | FROM requests 49 | WHERE identifier_for_vendor = ?1 50 | `, 51 | ) 52 | .bind("zzz-yyy-xxx") 53 | .first(); 54 | 55 | expect(request.identifier_for_vendor).toBe("zzz-yyy-xxx"); 56 | expect(request.status_code).toBe(200); 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /src/d1/index.ts: -------------------------------------------------------------------------------- 1 | import { Context } from "hono"; 2 | import { Bindings } from "../bindings"; 3 | import HeaderUtils from "../header_utils"; 4 | 5 | class D1 { 6 | constructor(readonly context: Context<{ Bindings: Bindings }>) {} 7 | 8 | async saveAnalyticsParams() { 9 | const userAgent = this.context.req.raw.headers.get("user-agent"); 10 | const cfConnectingIP = this.context.req.raw.headers.get("cf-connecting-ip"); 11 | const cfIPCountry = this.context.req.raw.headers.get("cf-ipcountry"); 12 | const xServiceId = this.context.req.raw.headers.get("x-gateway-service-id"); 13 | const xServiceName = this.context.req.raw.headers.get("x-gateway-service-name"); 14 | const xIdentifierForVendor = this.context.req.raw.headers.get("x-gateway-identifier-for-vendor"); 15 | const xBundleIdentifier = this.context.req.raw.headers.get("x-gateway-bundle-identifier"); 16 | const xBundleVersion = this.context.req.raw.headers.get("x-gateway-bundle-version"); 17 | const url = this.context.req.raw.url; 18 | const headers = new HeaderUtils(this.context.req.raw.headers).removeSensitiveHeaders().toJsonString(); 19 | const response = this.context.res.clone(); 20 | 21 | await this.context.env.DB.prepare( 22 | ` 23 | INSERT INTO requests (user_agent, 24 | cf_connecting_ip, 25 | cf_ip_country, 26 | service_id, 27 | service_name, 28 | identifier_for_vendor, 29 | bundle_identifier, 30 | url, 31 | headers, 32 | status_code, 33 | error, 34 | bundle_version 35 | ) 36 | VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12) 37 | `, 38 | ) 39 | .bind( 40 | userAgent, 41 | cfConnectingIP, 42 | cfIPCountry, 43 | xServiceId, 44 | xServiceName, 45 | xIdentifierForVendor, 46 | xBundleIdentifier, 47 | url, 48 | headers, 49 | response.status, 50 | response.ok ? null : await response.text(), 51 | xBundleVersion, 52 | ) 53 | .run(); 54 | } 55 | } 56 | 57 | export default D1; 58 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { Hono } from "hono"; 2 | import { TError, ServiceAuthType } from "./types"; 3 | import { streamResponse } from "./utils"; 4 | import { AppContext, Bindings, Variables } from "./bindings"; 5 | import HeaderUtils from "./header_utils"; 6 | import analytics from "./middleware/analytics"; 7 | import validate from "./middleware/validate"; 8 | import cron from "./crons/cron"; 9 | 10 | export const app = new Hono<{ Bindings: Bindings; Variables: Variables }>(); 11 | 12 | app.use("*", analytics()); 13 | app.use("*", validate()); 14 | 15 | app.all("*", async (context: AppContext) => { 16 | const clone = context.req.raw.clone(); 17 | const url = new URL(clone.url); 18 | const token = context.get("token"); 19 | 20 | // Get the x-gateway-* headers 21 | const xGatewayServiceHost = clone.headers.get("x-gateway-service-host")!; 22 | const xGatewayServiceAuthKey = clone.headers.get("x-gateway-service-auth-key"); 23 | const xGatewayServiceAuthType = clone.headers.get("x-gateway-service-auth-type"); 24 | const xGatewayAuthorizationPrefix = clone.headers.get("x-gateway-service-auth-prefix"); 25 | 26 | // Set the host to the proxied service host 27 | url.host = xGatewayServiceHost; 28 | 29 | // remove all x-gateway-* headers from the request 30 | const headers = new HeaderUtils(clone.headers).removeGatewayHeaders().get(); 31 | const tokenValue = xGatewayAuthorizationPrefix ? `${xGatewayAuthorizationPrefix} ${token}` : token; 32 | 33 | if (!xGatewayServiceAuthKey) { 34 | return context.json({ error: "x-gateway-service-auth-key is required!" }); 35 | } 36 | 37 | switch (xGatewayServiceAuthType) { 38 | case ServiceAuthType.HEADER: 39 | headers.append(xGatewayServiceAuthKey, tokenValue); 40 | break; 41 | case ServiceAuthType.QUERY: 42 | url.searchParams.append(xGatewayServiceAuthKey, tokenValue); 43 | break; 44 | default: 45 | return context.json({ error: "x-gateway-service-auth-type should be either of HEADER or QUERY" }); 46 | } 47 | 48 | // Make the request to the designated service 49 | const response = await fetch(url, { 50 | method: clone.method, 51 | body: clone.body, 52 | headers: headers, 53 | }); 54 | 55 | // If the response is not a stream forward it as it is. 56 | if (response.headers.get("Content-Type") !== "text/event-stream") { 57 | return response; 58 | } 59 | // If the response is a stream, forward it as a stream. 60 | return streamResponse(response.body); 61 | }); 62 | 63 | 64 | app.onError((err: Error, context: AppContext) => { 65 | return context.json({ error: err.message }, 500); 66 | }); 67 | 68 | export default { 69 | fetch: app.fetch, 70 | scheduled: cron, 71 | }; 72 | -------------------------------------------------------------------------------- /tests/compute_renderer.test.ts: -------------------------------------------------------------------------------- 1 | import { app } from "../src"; 2 | import { BINDINGS, getMockComputeRenderer, setInMemoryD1Database } from "./utils"; 3 | import { RequestModel } from "../src/d1/models"; 4 | 5 | describe("it should test all the proxied endpoints for compute renderer", () => { 6 | // This is a hack to make sure that the database is initialized before the tests are run 7 | beforeAll(async () => { 8 | BINDINGS["DB"] = await setInMemoryD1Database(); 9 | BINDINGS["API_COMPUTERENDER_COM_API_KEY"] = "compute-render-api-key"; 10 | }); 11 | 12 | // Intercept the request to be proxied to api.computerender.com 13 | beforeEach(() => { 14 | const formData = new FormData(); 15 | formData.append("prompt", "This is a test!"); 16 | 17 | getMockComputeRenderer() 18 | .intercept({ 19 | method: "POST", 20 | path: "/generate", 21 | headers: { 22 | "content-type": "application/x-www-form-urlencoded", 23 | authorization: `X-API-Key ${BINDINGS["API_COMPUTERENDER_COM_API_KEY"]}`, 24 | }, 25 | body: () => true, 26 | }) 27 | .reply(200, undefined, { 28 | headers: { 29 | "x-compute-renderer": "true", 30 | }, 31 | }); 32 | }); 33 | 34 | it("should prepend the authorization type value for authorization header", async () => { 35 | const context = new ExecutionContext(); 36 | const formData = new FormData(); 37 | formData.append("prompt", "This is a test!"); 38 | 39 | const response = await app.request( 40 | "/generate", 41 | { 42 | method: "POST", 43 | headers: { 44 | "x-gateway-service-host": "api.computerender.com", 45 | "x-gateway-service-auth-type": "HEADER", 46 | "x-gateway-service-auth-key": "authorization", 47 | "x-gateway-service-auth-prefix": "X-API-Key", 48 | "content-type": "application/x-www-form-urlencoded", 49 | "x-gateway-identifier-for-vendor": "compute_renderer", 50 | }, 51 | body: formData, 52 | }, 53 | BINDINGS, 54 | context, 55 | ); 56 | 57 | await getMiniflareWaitUntil(context); 58 | 59 | expect(response.status).toBe(200); 60 | expect(response.headers.get("x-compute-renderer")).toBe("true"); 61 | 62 | const { DB } = BINDINGS; 63 | const { results }: { results: RequestModel[] } = await DB.prepare( 64 | ` 65 | SELECT * 66 | FROM requests 67 | WHERE identifier_for_vendor = ?1 68 | `, 69 | ) 70 | .bind("compute_renderer") 71 | .all(); 72 | 73 | expect(results.length).toBe(1); 74 | expect(results[0].status_code).toBe(200); 75 | expect(results[0].identifier_for_vendor).toBe("compute_renderer"); 76 | }); 77 | }); 78 | -------------------------------------------------------------------------------- /tests/crons.test.ts: -------------------------------------------------------------------------------- 1 | import { BINDINGS, setInMemoryD1Database } from "./utils"; 2 | import delete_old_data_cron from "../src/crons/delete_old_data_cron"; 3 | import { Bindings } from "../src/bindings"; 4 | import { D1ResultMeta } from "../src/types"; 5 | 6 | describe("Test all the cron jobs", () => { 7 | beforeAll(async () => { 8 | BINDINGS["DB"] = await setInMemoryD1Database(); 9 | }); 10 | 11 | // seed the database with 100 rows to test cron jobs 12 | beforeEach(async () => { 13 | const db = BINDINGS["DB"]; 14 | const stmt = db.prepare(` 15 | INSERT INTO requests(user_agent, 16 | created_at, 17 | cf_connecting_ip, 18 | cf_ip_country, 19 | service_id, 20 | service_name, 21 | identifier_for_vendor, 22 | bundle_identifier, 23 | url, 24 | headers, 25 | status_code, 26 | error, 27 | bundle_version) 28 | VALUES ('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Safari/537.36', 29 | datetime('now', '-1 day'), 30 | '192.168.1.1', 31 | 'US', 32 | 'com.apple.assistant.assistantd', 33 | 'Assistant', 34 | 'com.apple.assistant.assistantd.00000000-0000-0000-0000-000000000000', 35 | 'com.apple.assistant.assistantd', 36 | 'https://www.open_ai.com', 37 | '{ 38 | "Accept": "*/*", 39 | "Accept-Encoding": "gzip, deflate, br", 40 | "Accept-Language": "en-US,en;q=0.9", 41 | "Connection": "keep-alive" 42 | }', 43 | 200, 44 | null, 45 | 'unknown version') 46 | `); 47 | 48 | await db.batch(Array(100).fill(stmt)); 49 | }); 50 | 51 | it("should test delete_old_data_cron to delete data older than X days", async () => { 52 | // Change the value of DELETE_OLD_DATA_BEFORE to delete data older than 12 hours 53 | BINDINGS["DELETE_OLD_DATA_BEFORE"] = "-12 hours"; 54 | 55 | const meta: D1ResultMeta = await delete_old_data_cron(BINDINGS); 56 | 57 | expect(meta.changes).toBe(100); 58 | }); 59 | 60 | it("should not delete data that is not older than X days", async () => { 61 | // Change the value of DELETE_OLD_DATA_BEFORE to delete data older than 7 days 62 | BINDINGS["DELETE_OLD_DATA_BEFORE"] = "-7 day"; 63 | 64 | const meta: D1ResultMeta = await delete_old_data_cron(BINDINGS); 65 | 66 | expect(meta.changes).toBe(0); 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /tests/db.log.test.ts: -------------------------------------------------------------------------------- 1 | import { BINDINGS, getMockOpenAI, setInMemoryD1Database } from "./utils"; 2 | import { app } from "../src"; 3 | import { RequestModel } from "../src/d1/models"; 4 | 5 | describe("it should have all the request logged to the requests table", () => { 6 | // This is a hack to make sure that the database is initialized before the tests are run 7 | beforeAll(async () => { 8 | BINDINGS["DB"] = await setInMemoryD1Database(); 9 | }); 10 | 11 | it("should have all the successful request logged in the database", async () => { 12 | const { DB } = BINDINGS; 13 | const context = new ExecutionContext(); 14 | 15 | for (let i = 0; i < 10; i++) { 16 | const status = i % 2 === 0 ? 200 : 400; 17 | const body = i % 2 === 0 ? "Hello World!" : "Bad Request!"; 18 | 19 | getMockOpenAI() 20 | .intercept({ 21 | method: "POST", 22 | path: `/v1/chat/completions?temperature=${0.1 * i}`, 23 | headers: { 24 | "content-type": "application/json", 25 | authorization: `Bearer ${BINDINGS["API_OPENAI_COM_API_KEY"]}`, 26 | }, 27 | body: undefined, 28 | }) 29 | .reply(status, body); 30 | 31 | await app.request( 32 | `/v1/chat/completions?temperature=${0.1 * i}`, 33 | { 34 | method: "POST", 35 | headers: { 36 | "x-gateway-service-host": "api.openai.com", 37 | "x-gateway-service-token": BINDINGS["API_OPENAI_COM_API_KEY"], 38 | "x-gateway-service-auth-type": "HEADER", 39 | "x-gateway-service-auth-key": "Authorization", 40 | "x-gateway-service-auth-prefix": "Bearer", 41 | "content-type": "application/json", 42 | "x-gateway-identifier-for-vendor": "logging-successful-requests", 43 | }, 44 | body: JSON.stringify({ 45 | model: "gpt-3.5-turbo", 46 | messages: [{ role: "user", content: `This is a test for ${i}!` }], 47 | temperature: 0.1 * i, 48 | }), 49 | }, 50 | BINDINGS, 51 | context, 52 | ); 53 | } 54 | 55 | await getMiniflareWaitUntil(context); 56 | 57 | const { results }: { results: RequestModel[] } = await DB.prepare( 58 | ` 59 | SELECT * 60 | FROM requests 61 | WHERE identifier_for_vendor = ?1 62 | `, 63 | ) 64 | .bind("logging-successful-requests") 65 | .all(); 66 | 67 | // Test if the requests are logged in the database 68 | expect(results.length).toBeGreaterThanOrEqual(10); 69 | expect(results.filter((request) => request.status_code === 200).length).toBeGreaterThanOrEqual(5); 70 | 71 | // Test if the error is logged in the database 72 | const failed = results.filter((request) => request.status_code === 400); 73 | expect(failed.length).toBeGreaterThanOrEqual(5); 74 | expect(failed.map((request) => request.error)).toStrictEqual(Array.from({ length: 5 }, () => "Bad Request!")); 75 | }); 76 | }); 77 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | 3 | logs 4 | _.log 5 | npm-debug.log_ 6 | yarn-debug.log* 7 | yarn-error.log* 8 | lerna-debug.log* 9 | .pnpm-debug.log* 10 | 11 | # Diagnostic reports (https://nodejs.org/api/report.html) 12 | 13 | report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json 14 | 15 | # Runtime data 16 | 17 | pids 18 | _.pid 19 | _.seed 20 | \*.pid.lock 21 | 22 | # Directory for instrumented libs generated by jscoverage/JSCover 23 | 24 | lib-cov 25 | 26 | # Coverage directory used by tools like istanbul 27 | 28 | coverage 29 | \*.lcov 30 | 31 | # nyc test coverage 32 | 33 | .nyc_output 34 | 35 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 36 | 37 | .grunt 38 | 39 | # Bower dependency directory (https://bower.io/) 40 | 41 | bower_components 42 | 43 | # node-waf configuration 44 | 45 | .lock-wscript 46 | 47 | # Compiled binary addons (https://nodejs.org/api/addons.html) 48 | 49 | build/Release 50 | 51 | # Dependency directories 52 | 53 | node_modules/ 54 | jspm_packages/ 55 | 56 | # Snowpack dependency directory (https://snowpack.dev/) 57 | 58 | web_modules/ 59 | 60 | # TypeScript cache 61 | 62 | \*.tsbuildinfo 63 | 64 | # Optional npm cache directory 65 | 66 | .npm 67 | 68 | # Optional eslint cache 69 | 70 | .eslintcache 71 | 72 | # Optional stylelint cache 73 | 74 | .stylelintcache 75 | 76 | # Microbundle cache 77 | 78 | .rpt2_cache/ 79 | .rts2_cache_cjs/ 80 | .rts2_cache_es/ 81 | .rts2_cache_umd/ 82 | 83 | # Optional REPL history 84 | 85 | .node_repl_history 86 | 87 | # Output of 'npm pack' 88 | 89 | \*.tgz 90 | 91 | # Yarn Integrity file 92 | 93 | .yarn-integrity 94 | 95 | # dotenv environment variable files 96 | 97 | .env 98 | .env.development.local 99 | .env.test.local 100 | .env.production.local 101 | .env.local 102 | 103 | # parcel-bundler cache (https://parceljs.org/) 104 | 105 | .cache 106 | .parcel-cache 107 | 108 | # Next.js build output 109 | 110 | .next 111 | out 112 | 113 | # Nuxt.js build / generate output 114 | 115 | .nuxt 116 | dist 117 | 118 | # Gatsby files 119 | 120 | .cache/ 121 | 122 | # Comment in the public line in if your project uses Gatsby and not Next.js 123 | 124 | # https://nextjs.org/blog/next-9-1#public-directory-support 125 | 126 | # public 127 | 128 | # vuepress build output 129 | 130 | .vuepress/dist 131 | 132 | # vuepress v2.x temp and cache directory 133 | 134 | .temp 135 | .cache 136 | 137 | # Docusaurus cache and generated files 138 | 139 | .docusaurus 140 | 141 | # Serverless directories 142 | 143 | .serverless/ 144 | 145 | # FuseBox cache 146 | 147 | .fusebox/ 148 | 149 | # DynamoDB Local files 150 | 151 | .dynamodb/ 152 | 153 | # TernJS port file 154 | 155 | .tern-port 156 | 157 | # Stores VSCode versions used for testing VSCode extensions 158 | 159 | .vscode-test 160 | 161 | # yarn v2 162 | 163 | .yarn/cache 164 | .yarn/unplugged 165 | .yarn/build-state.yml 166 | .yarn/install-state.gz 167 | .pnp.\* 168 | 169 | # wrangler project 170 | 171 | .dev.vars 172 | .wrangler/ 173 | 174 | .idea 175 | -------------------------------------------------------------------------------- /tests/open_ai.test.ts: -------------------------------------------------------------------------------- 1 | import { app } from "../src"; 2 | import { BINDINGS, getMockOpenAI, setInMemoryD1Database } from "./utils"; 3 | import { RequestModel } from "../src/d1/models"; 4 | 5 | describe("it should test all the proxied endpoints for openai", () => { 6 | // Create an in-memory sqlite database 7 | beforeAll(async () => { 8 | BINDINGS["DB"] = await setInMemoryD1Database(); 9 | BINDINGS["API_OPENAI_COM_API_KEY"] = "sk-1234567890"; 10 | }); 11 | 12 | // Intercept the request to the proxied service when the API key is provided in the request headers 13 | beforeEach(() => { 14 | getMockOpenAI() 15 | .intercept({ 16 | method: "POST", 17 | path: "/v1/chat/completions", 18 | headers: { 19 | "content-type": "application/json", 20 | authorization: `Bearer ${BINDINGS["API_OPENAI_COM_API_KEY"]}`, 21 | }, 22 | body: undefined, 23 | }) 24 | .reply(200); 25 | }); 26 | 27 | it("should use the API key for proxied service from env variables if not provided in request headers.", async () => { 28 | const response = await app.request( 29 | "/v1/chat/completions", 30 | { 31 | method: "POST", 32 | headers: { 33 | "x-gateway-service-host": "api.openai.com", 34 | "x-gateway-service-auth-type": "HEADER", 35 | "x-gateway-service-auth-key": "Authorization", 36 | "x-gateway-service-auth-prefix": "Bearer", 37 | "x-gateway-identifier-for-vendor": "xxx-yyy-zzz", 38 | "content-type": "application/json", 39 | }, 40 | body: JSON.stringify({ 41 | model: "gpt-3.5-turbo", 42 | messages: [{ role: "user", content: "Say this is a test!" }], 43 | temperature: 0.7, 44 | }), 45 | }, 46 | BINDINGS, 47 | new ExecutionContext(), 48 | ); 49 | 50 | expect(response.status).toBe(200); 51 | 52 | const { DB } = BINDINGS; 53 | const request: RequestModel = await DB.prepare( 54 | ` 55 | SELECT * 56 | FROM requests 57 | WHERE identifier_for_vendor = ?1 58 | `, 59 | ) 60 | .bind("xxx-yyy-zzz") 61 | .first(); 62 | 63 | expect(request.identifier_for_vendor).toBe("xxx-yyy-zzz"); 64 | expect(request.status_code).toBe(200); 65 | }); 66 | 67 | it("should use the API key provided in headers to proxy the request to designated service", async () => { 68 | const response = await app.request( 69 | "/v1/chat/completions", 70 | { 71 | method: "POST", 72 | headers: { 73 | "x-gateway-service-host": "api.openai.com", 74 | "x-gateway-service-token": BINDINGS["API_OPENAI_COM_API_KEY"], 75 | "x-gateway-service-auth-type": "HEADER", 76 | "x-gateway-service-auth-key": "Authorization", 77 | "x-gateway-service-auth-prefix": "Bearer", 78 | "content-type": "application/json", 79 | "x-gateway-identifier-for-vendor": "aaa-bbb-ccc", 80 | "x-gateway-bundle-version": "1.0.0", 81 | }, 82 | body: JSON.stringify({ 83 | model: "gpt-3.5-turbo", 84 | messages: [{ role: "user", content: "Say this is a test!" }], 85 | temperature: 0.7, 86 | }), 87 | }, 88 | BINDINGS, 89 | new ExecutionContext(), 90 | ); 91 | 92 | expect(response.status).toBe(200); 93 | 94 | const { DB } = BINDINGS; 95 | const request: RequestModel = await DB.prepare( 96 | ` 97 | SELECT * 98 | FROM requests 99 | WHERE identifier_for_vendor = ?1 100 | `, 101 | ) 102 | .bind("aaa-bbb-ccc") 103 | .first(); 104 | 105 | expect(request.identifier_for_vendor).toBe("aaa-bbb-ccc"); 106 | expect(request.status_code).toBe(200); 107 | expect(request.bundle_version).toBe("1.0.0"); 108 | }); 109 | }); 110 | -------------------------------------------------------------------------------- /tests/index.test.ts: -------------------------------------------------------------------------------- 1 | import { app } from "../src"; 2 | import { BINDINGS, getMockOpenAI, setInMemoryD1Database } from "./utils"; 3 | import { RequestModel } from "../src/d1/models"; 4 | 5 | describe("Test if a request has all the details required to proxy the request to upstream service", () => { 6 | // Create an in-memory sqlite database 7 | beforeAll(async () => { 8 | BINDINGS["DB"] = await setInMemoryD1Database(); 9 | BINDINGS["API_OPENAI_COM_API_KEY"] = "sk-1234567890"; 10 | BINDINGS["API_SHODAN_IO_API_KEY"] = "shodan-api-key"; 11 | BINDINGS["API_COMPUTERENDER_COM_API_KEY"] = "compute-render-api-key"; 12 | }); 13 | 14 | // Intercept the request to the proxied service when the API key is provided in the request headers and is not a Bearer token 15 | beforeEach(() => { 16 | getMockOpenAI() 17 | .intercept({ 18 | method: "POST", 19 | path: "/v1/chat/completions", 20 | headers: { 21 | "content-type": "application/json", 22 | "x-api-key": BINDINGS["API_OPENAI_COM_API_KEY"], 23 | }, 24 | body: undefined, 25 | }) 26 | .reply(200); 27 | }); 28 | 29 | it("should throw and error if x-gateway-service-host isn't provided in headers.", async () => { 30 | const context = new ExecutionContext(); 31 | const response = await app.request( 32 | "/v1/chat/completions", 33 | { 34 | method: "POST", 35 | headers: { 36 | "x-gateway-service-token": BINDINGS["API_OPENAI_COM_API_KEY"], 37 | "x-gateway-service-auth-type": "HEADER", 38 | "x-gateway-service-auth-key": "Authorization", 39 | "content-type": "application/json", 40 | "x-gateway-identifier-for-vendor": "x-gateway-service-host-not-found", 41 | }, 42 | body: JSON.stringify({ 43 | model: "gpt-3.5-turbo", 44 | messages: [{ role: "user", content: "Say this is a test!" }], 45 | temperature: 0.7, 46 | }), 47 | }, 48 | BINDINGS, 49 | context, 50 | ); 51 | 52 | await getMiniflareWaitUntil(context); 53 | 54 | expect(response.status).toBe(400); 55 | expect(await response.json()).toEqual({ error: "x-gateway-service-host header is required." }); 56 | 57 | const { DB } = BINDINGS; 58 | 59 | const request: RequestModel = await DB.prepare( 60 | ` 61 | SELECT * 62 | FROM requests 63 | WHERE identifier_for_vendor = ?1 64 | `, 65 | ) 66 | .bind("x-gateway-service-host-not-found") 67 | .first(); 68 | 69 | expect(request.status_code).toBe(400); 70 | expect(JSON.parse(request.error)).toStrictEqual({ error: "x-gateway-service-host header is required." }); 71 | }); 72 | 73 | it("should throw and error if the API key is not found in header and is not set in env.", async () => { 74 | const context = new ExecutionContext(); 75 | const response = await app.request( 76 | "/v1/chat/completions", 77 | { 78 | method: "POST", 79 | headers: { 80 | "x-gateway-service-host": "api.monapi.io", 81 | "x-gateway-service-auth-type": "HEADER", 82 | "x-gateway-service-auth-key": "Authorization", 83 | "content-type": "application/json", 84 | "x-gateway-identifier-for-vendor": "api-key-not-found", 85 | }, 86 | body: JSON.stringify({ 87 | model: "gpt-3.5-turbo", 88 | messages: [{ role: "user", content: "Say this is a test!" }], 89 | temperature: 0.7, 90 | }), 91 | }, 92 | BINDINGS, 93 | context, 94 | ); 95 | 96 | await getMiniflareWaitUntil(context); 97 | 98 | expect(response.status).toBe(400); 99 | expect(await response.json()).toEqual({ 100 | error: "Cannot find API key for proxied service! Either provide it in the request headers or set it as an environment variable.", 101 | }); 102 | 103 | const { DB } = BINDINGS; 104 | const request: RequestModel = await DB.prepare( 105 | ` 106 | SELECT * 107 | FROM requests 108 | WHERE identifier_for_vendor = ?1 109 | `, 110 | ) 111 | .bind("api-key-not-found") 112 | .first(); 113 | 114 | expect(request.status_code).toBe(400); 115 | expect(request.identifier_for_vendor).toBe("api-key-not-found"); 116 | expect(JSON.parse(request.error)).toStrictEqual({ 117 | error: "Cannot find API key for proxied service! Either provide it in the request headers or set it as an environment variable.", 118 | }); 119 | }); 120 | }); 121 | -------------------------------------------------------------------------------- /tests/cloudflare.ai.gateway.test.ts: -------------------------------------------------------------------------------- 1 | import { app } from "../src"; 2 | import { RequestModel } from "../src/d1/models"; 3 | import { BINDINGS, getMockCloudflareAIGateway, setInMemoryD1Database } from "./utils"; 4 | 5 | describe("Cloudflare AI Gateway", () => { 6 | beforeAll(async () => { 7 | BINDINGS["DB"] = await setInMemoryD1Database(); 8 | BINDINGS["GATEWAY_AI_CLOUDFLARE_COM_API_KEY_MISTRAL"] = "cloudflare-ai-mistral"; 9 | }); 10 | 11 | beforeEach(() => { 12 | getMockCloudflareAIGateway() 13 | .intercept({ 14 | method: "POST", 15 | path: "/v1/acc_123/gateway_123/openai/chat/completions", 16 | headers: { 17 | "content-type": "application/json", 18 | Authorization: "Bearer cloudflare-ai-mistral", 19 | }, 20 | body: undefined, 21 | }) 22 | .reply(200); 23 | }); 24 | 25 | it("should response with a failure response if the service type is DIRECT and a proxy is provided", async () => { 26 | const response = await app.request( 27 | "/v1/acc_123/gateway_123/openai/chat/completions", 28 | { 29 | method: "POST", 30 | headers: { 31 | "x-gateway-service-host": "gateway.ai.cloudflare.com", 32 | "x-gateway-service-auth-type": "HEADER", 33 | "x-gateway-service-auth-key": "Authorization", 34 | "x-gateway-service-auth-prefix": "Bearer", 35 | "x-gateway-identifier-for-vendor": "ccc-ddd-eee", 36 | "x-gateway-service-proxy": "MISTRAL", 37 | "content-type": "application/json", 38 | }, 39 | body: JSON.stringify({ 40 | model: "gpt-3.5-turbo", 41 | messages: [{ role: "user", content: "Say this is a test!" }], 42 | temperature: 0.7, 43 | }), 44 | }, 45 | BINDINGS, 46 | new ExecutionContext(), 47 | ); 48 | 49 | expect(response.status).toEqual(400); 50 | expect(await response.json()).toMatchObject({ 51 | error: "x-gateway-service-proxy header is not allowed for direct service type.", 52 | }); 53 | }); 54 | 55 | it("should respond with a failure response if the service type is GATEWAY and no proxy is provided", async () => { 56 | const response = await app.request( 57 | "/v1/acc_123/gateway_123/openai/chat/completions", 58 | { 59 | method: "POST", 60 | headers: { 61 | "x-gateway-service-host": "gateway.ai.cloudflare.com", 62 | "x-gateway-service-auth-type": "HEADER", 63 | "x-gateway-service-auth-key": "Authorization", 64 | "x-gateway-service-auth-prefix": "Bearer", 65 | "x-gateway-identifier-for-vendor": "ccc-ddd-eee", 66 | "x-gateway-service-type": "GATEWAY", 67 | "content-type": "application/json", 68 | }, 69 | body: JSON.stringify({ 70 | model: "gpt-3.5-turbo", 71 | messages: [{ role: "user", content: "Say this is a test!" }], 72 | temperature: 0.7, 73 | }), 74 | }, 75 | BINDINGS, 76 | new ExecutionContext(), 77 | ); 78 | 79 | expect(response.status).toEqual(400); 80 | expect(await response.json()).toMatchObject({ 81 | error: "x-gateway-service-proxy header is required.", 82 | }); 83 | }); 84 | 85 | it("should use the API key for cloudflare gateway proxied service from env variables if not provided in request headers.", async () => { 86 | const response = await app.request( 87 | "/v1/acc_123/gateway_123/openai/chat/completions", 88 | { 89 | method: "POST", 90 | headers: { 91 | "x-gateway-service-host": "gateway.ai.cloudflare.com", 92 | "x-gateway-service-auth-type": "HEADER", 93 | "x-gateway-service-auth-key": "Authorization", 94 | "x-gateway-service-auth-prefix": "Bearer", 95 | "x-gateway-identifier-for-vendor": "ccc-ddd-eee", 96 | "x-gateway-service-type": "GATEWAY", 97 | "x-gateway-service-proxy": "Mistral", 98 | "content-type": "application/json", 99 | }, 100 | body: JSON.stringify({ 101 | model: "gpt-3.5-turbo", 102 | messages: [{ role: "user", content: "Say this is a test!" }], 103 | temperature: 0.7, 104 | }), 105 | }, 106 | BINDINGS, 107 | new ExecutionContext(), 108 | ); 109 | 110 | expect(response.status).toBe(200); 111 | 112 | const { DB } = BINDINGS; 113 | const request: RequestModel = await DB.prepare( 114 | ` 115 | SELECT * 116 | FROM requests 117 | WHERE identifier_for_vendor = ?1 118 | `, 119 | ) 120 | .bind("ccc-ddd-eee") 121 | .first(); 122 | 123 | expect(request.identifier_for_vendor).toBe("ccc-ddd-eee"); 124 | expect(request.status_code).toBe(200); 125 | }); 126 | 127 | it("should throw an error if the service type is not valid", async () => { 128 | const response = await app.request( 129 | "/v1/acc_123/gateway_123/openai/chat/completions", 130 | { 131 | method: "POST", 132 | headers: { 133 | "x-gateway-service-type": "INVALID", 134 | }, 135 | }, 136 | BINDINGS, 137 | new ExecutionContext(), 138 | ); 139 | 140 | expect(response.status).toEqual(500); 141 | expect(await response.json()).toMatchObject({ 142 | error: "Invalid service type", 143 | }); 144 | }); 145 | }); 146 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gateway 2 | 3 | * Run the development `server npm run start`` 4 | * Deploy your application `npm run deploy`` 5 | * Read the documentation https://developers.cloudflare.com/workers 6 | * Stuck? Join us at https://discord.gg/cloudflaredev 7 | 8 | ## Configuration 9 | 10 | | Header | Description | 11 | |---------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 12 | | `x-gateway-service-host` | The host for the API request | 13 | | `x-gateway-service-token` | The API token to use if not in the environment variables | 14 | | `x-gateway-service-auth-key` | The authorization key for the request. This is used in combined with `x-gateway-service-auth-type`. If `x-gateway-service-auth-type` is `HEADER` then this value will be used as the HTTP header; if `x-gateway-service-auth-type` then this value will be the query field. | 15 | | `x-gateway-service-auth-type` | The authorization type. This is either `HEADER` to add the token to the requests header or `QUERY` to add it to the requests parameters. | 16 | | `x-gateway-service-auth-prefix` | Any prefix for the token. A space is appended after this prefix. | 17 | | `x-gateway-service-type` | Indicates whether the downstream service is `GATEWAY` or `DIRECT`. The default is `DIRECT`. | 18 | | `x-gateway-service-proxy` | Required only when `x-gateway-service-type` is `GATEWAY`. The format can be anything, provided the environment has a secret named `{x-gateway-service-host}_API_KEY_{x-gateway-service-proxy}`. For example, if the GATEWAY is `CLOUDFLARE AI` and the service is `MISTRAL`, then `x-gateway-service-proxy` can be `MISTRAL`. Note that this proxy header will be used to resolve the Cloudflare secret in the environment by replacing all non-alphanumeric characters with an underscore (`_`). | 19 | 20 | ### Example 21 | 22 | * If you want to use [Bearer/token authentication](https://swagger.io/docs/specification/authentication/bearer-authentication/) then you will need to set the following headers appropriately: 23 | 24 | | Header | Value | 25 | | ------ | ----- | 26 | | `x-gateway-service-auth-key` | `Authorization` | 27 | | `x-gateway-service-auth-type` | `HEADER` | 28 | | `x-gateway-service-auth-prefix` | `Bearer` | 29 | 30 | * If you want to use a gateway service where the host remains constant but different API keys are used depending on the specific service within the gateway, follow these steps. 31 | For example, if you're using the Cloudflare AI gateway with the OpenAI service, and you have the Cloudflare secret for this service set as `GATEWAY_AI_CLOUDFLARE_COM_API_KEY_OPEN_AI = sk-your-api-key`, then make a request with the following headers: 32 | 33 | | Header | Value | 34 | |---------------------------------|-----------------------------| 35 | | `x-gateway-service-auth-key` | `Authorization` | 36 | | `x-gateway-service-auth-type` | `HEADER` | 37 | | `x-gateway-service-auth-prefix` | `Bearer` | 38 | | `x-gateway-service-type` | `GATEWAY` | 39 | | `x-gateway-service-host` | `gateway.ai.cloudflare.com` | 40 | | `x-gateway-service-proxy` | `OPEN AI` | 41 | 42 | ```http request 43 | POST https://gateway.your-subdomain.workers.dev/v1/{account_id}/{gateway_id}/openai/chat/completions 44 | x-gateway-service-auth-key: Authorization 45 | x-gateway-service-auth-type: HEADER 46 | x-gateway-service-auth-prefix: Bearer 47 | x-gateway-service-type: GATEWAY 48 | x-gateway-service-proxy: OPEN AI 49 | x-gateway-service-host: gateway.ai.cloudflare.com 50 | Content-Type: application/json 51 | 52 | { 53 | "model": "gpt-3.5-turbo", 54 | "messages": [ 55 | { 56 | "role": "user", 57 | "content": "What is AI?" 58 | } 59 | ] 60 | } 61 | ``` 62 | 63 | 64 | ## Errors 65 | 66 | If configured incorrectly a HTTP 400 JSON response will be returned. The format will be `{ "error": "" }`. 67 | 68 | ## Logging 69 | 70 | Metadata is collected for every request. This metadata is used to detect abuse and to help prevent unauthorized access. 71 | 72 | All headers are recorded as a JSON blob. Known sensitive headers are stripped from the blob. In addition, the following request header fields are recorded separately: 73 | 74 | | Header | Description | 75 | | ------ | ----------- | 76 | | `user-agent` | The user agent. | 77 | | [`cf-connecting-ip`](https://developers.cloudflare.com/fundamentals/reference/http-request-headers/#cf-connecting-ip) | The client IP address connecting to Cloudflare to the origin web server. | 78 | | [`cf-ipcountry`](https://developers.cloudflare.com/fundamentals/reference/http-request-headers/#cf-ipcountry) | The two-character country code of the originating visitor’s country. | 79 | | `x-gateway-service-id` | The requested service's identifier. | 80 | | `x-gateway-service-name` | The requested service's human readable name. | 81 | | `x-gateway-identifier-for-vendor` | An alphanumeric string that uniquely identifies a device to the connecting application’s vendor. | 82 | | `x-gateway-bundle-identifier` | The connecting application's bundle identifier. | 83 | | `x-gateway-bundle-version` | The connecting applications version number. | 84 | 85 | The following is also recorded: 86 | * Full URL of the request 87 | * Responses status code 88 | * Any server specific errors 89 | 90 | ## Data Retention 91 | 92 | All metadata is kept for a configured amount of period. Beyond this timeframe, the metadata undergoes permanent deletion with **no backup procedures in place.** 93 | 94 | To achieve this, a cron job executes every Sunday 12:00 AM(configurable), targeting metadata older than configured period for deletion. The configuration for this cron job is stored in the [`wrangler.toml`](./wrangler.toml) file, specifically under the `triggers` section. 95 | 96 | The cron job is designed to respect the `DELETE_OLD_DATA_BEFORE` environment variable. If this variable is not set, the cron job will intentionally **generate an SQL error**, preventing the deletion of any data. 97 | 98 | #### DELETE_OLD_DATA_BEFORE variable Format 99 | 100 | ``` 101 | [sign] quantity unit 102 | ``` 103 | 104 | - `sign`: Optional. Use `+` for addition and `-` for subtraction. 105 | - `quantity`: Numeric value. 106 | - `unit`: Time unit (`year(s)`, `month(s)`, `day(s)`, `hour(s)`, `minute(s)`, `second(s)`). 107 | 108 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | 5 | /* Projects */ 6 | // "incremental": true, /* Enable incremental compilation */ 7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 8 | // "tsBuildInfoFile": "./", /* Specify the folder for .tsbuildinfo incremental compilation files. */ 9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects */ 10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 12 | 13 | /* Language and Environment */ 14 | "target": "es2021", 15 | /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ "lib": ["es2021"], 16 | /* Specify a set of bundled library declaration files that describe the target runtime environment. */ "jsx": "react", 17 | /* Specify what JSX code is generated. */ // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ 18 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 19 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h' */ 20 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 21 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using `jsx: react-jsx*`.` */ 22 | // "reactNamespace": "", /* Specify the object invoked for `createElement`. This only applies when targeting `react` JSX emit. */ 23 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 24 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 25 | 26 | /* Modules */ 27 | "module": "es2022", 28 | /* Specify what module code is generated. */ // "rootDir": "./", /* Specify the root folder within your source files. */ 29 | "moduleResolution": "node", 30 | /* Specify how TypeScript looks up a file from a given module specifier. */ // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 31 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 32 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 33 | // "typeRoots": [], /* Specify multiple folders that act like `./node_modules/@types`. */ 34 | "types": ["@cloudflare/workers-types", "jest", "jest-environment-miniflare/globals"], 35 | /* Specify type package names to be included without being referenced in a source file. */ // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 36 | "resolveJsonModule": true, 37 | /* Enable importing .json files */ // "noResolve": true, /* Disallow `import`s, `require`s or ``s from expanding the number of files TypeScript should add to a project. */ 38 | 39 | /* JavaScript Support */ 40 | "allowJs": true, 41 | /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */ "checkJs": false, 42 | /* Enable error reporting in type-checked JavaScript files. */ // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from `node_modules`. Only applicable with `allowJs`. */ 43 | 44 | /* Emit */ 45 | // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 46 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 47 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 48 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 49 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If `declaration` is true, also designates a file that bundles all .d.ts output. */ 50 | // "outDir": "./", /* Specify an output folder for all emitted files. */ 51 | // "removeComments": true, /* Disable emitting comments. */ 52 | "noEmit": true, 53 | /* Disable emitting files from a compilation. */ // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 54 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types */ 55 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 56 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 57 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 58 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 59 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 60 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 61 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 62 | // "stripInternal": true, /* Disable emitting declarations that have `@internal` in their JSDoc comments. */ 63 | // "noEmitHelpers": true, /* Disable generating custom helper functions like `__extends` in compiled output. */ 64 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 65 | // "preserveConstEnums": true, /* Disable erasing `const enum` declarations in generated code. */ 66 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 67 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ 68 | 69 | /* Interop Constraints */ 70 | "isolatedModules": true, 71 | /* Ensure that each file can be safely transpiled without relying on other imports. */ "allowSyntheticDefaultImports": true, 72 | /* Allow 'import x from y' when a module doesn't have a default export. */ // "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */, 73 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 74 | "forceConsistentCasingInFileNames": true, 75 | /* Ensure that casing is correct in imports. */ /* Type Checking */ "strict": true, 76 | /* Enable all strict type-checking options. */ // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied `any` type.. */ 77 | // "strictNullChecks": true, /* When type checking, take into account `null` and `undefined`. */ 78 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 79 | // "strictBindCallApply": true, /* Check that the arguments for `bind`, `call`, and `apply` methods match the original function. */ 80 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 81 | // "noImplicitThis": true, /* Enable error reporting when `this` is given the type `any`. */ 82 | // "useUnknownInCatchVariables": true, /* Type catch clause variables as 'unknown' instead of 'any'. */ 83 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 84 | // "noUnusedLocals": true, /* Enable error reporting when a local variables aren't read. */ 85 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read */ 86 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 87 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 88 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 89 | // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ 90 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 91 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type */ 92 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 93 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 94 | 95 | /* Completeness */ 96 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 97 | "skipLibCheck": true 98 | /* Skip type checking all .d.ts files. */ 99 | } 100 | } 101 | --------------------------------------------------------------------------------