@stuntman/shared
Version:
Stuntman - HTTP proxy / mock shared types and utils
253 lines (217 loc) • 7.41 kB
text/typescript
/* eslint-disable @typescript-eslint/no-unsafe-function-type */
import config from './stuntmanConfig';
import { dirname } from 'path';
import { fileURLToPath } from 'url';
export * from './constants';
export * from './appError';
export * from './logger';
export * from './stringify';
export * from './rawHeaders';
export * from './gqlParser';
export * from './escapeStringRegexp';
export * from './errorToLog';
import fs from 'fs';
export const stuntmanConfig = config;
const __dirname = dirname(fileURLToPath(import.meta.url)).replace(/\/src$/, '/dist');
// TODO this file read sucks
export const INDEX_DTS = fs.readFileSync(`${__dirname}/index.d.ts`, 'utf-8');
type NonObject = string | number | boolean | symbol | undefined | null | any[];
export type SharedProps<T1, T2> = Pick<T1 | T2, Extract<keyof T1, keyof T2>>;
export type WithRequiredProperty<Type, Key extends keyof Type> = Type & {
[Property in Key]-?: Type[Property];
};
interface SerializableTypesRecord<T> {
[k: string | number]: T;
}
export type RecursivePartial<T> = {
[P in keyof T]?: T[P] extends (infer U)[] ? RecursivePartial<U>[] : T[P] extends object ? RecursivePartial<T[P]> : T[P];
};
type SerializableTypes =
| string
| number
| boolean
| undefined
| null
| RegExp
| SerializableTypes[]
| SerializableTypesRecord<SerializableTypes>;
export enum HttpCode {
OK = 200,
NO_CONTENT = 204,
BAD_REQUEST = 400,
UNAUTHORIZED = 401,
NOT_FOUND = 404,
CONFLICT = 409,
UNPROCESSABLE_ENTITY = 422,
INTERNAL_SERVER_ERROR = 500,
}
export type LocalVariables = Record<string, SerializableTypes>;
export type RuleMatchResult =
| boolean
| { result: boolean; enableRuleIds?: string[]; disableRuleIds?: string[]; description?: string; labels?: string[] };
export type RemotableFunction<T extends Function> = {
localFn: T;
localVariables?: LocalVariables;
};
export type SerializedRemotableFunction = {
localFn: string;
localVariables?: string;
remoteFn: string;
};
type WithRemotableFunctions<Type> = {
[PropertyKey in keyof Type]: Extract<Type[PropertyKey], Function> extends never
? Exclude<Type[PropertyKey], NonObject> extends never
? Type[PropertyKey]
: WithRemotableFunctions<Exclude<Type[PropertyKey], NonObject>>
: Exclude<Type[PropertyKey], Function> | RemotableFunction<Extract<Type[PropertyKey], Function>>;
};
export type WithSerializedFunctions<Type> = {
[PropertyKey in keyof Type]: Extract<Type[PropertyKey], RemotableFunction<Function>> extends never
? Exclude<Type[PropertyKey], NonObject> extends never
? Type[PropertyKey]
: WithSerializedFunctions<Exclude<Type[PropertyKey], NonObject>>
: Exclude<Type[PropertyKey], RemotableFunction<Function>> | SerializedRemotableFunction;
};
export interface RawHeadersInterface extends Array<string> {
get: (name: string) => string | undefined;
set: (name: string, value: string) => void;
has: (name: string, value?: string) => boolean;
add: (name: string, value: string) => void;
remove: (name: string) => void;
toHeaderPairs: () => readonly [string, string][];
}
export const HTTP_METHODS = ['GET', 'HEAD', 'POST', 'PUT', 'DELETE', 'CONNECT', 'OPTIONS', 'TRACE', 'PATCH'] as const;
export type HttpMethod = (typeof HTTP_METHODS)[number];
export type BaseRequest = {
rawHeaders: RawHeadersInterface;
url: string;
body?: any;
method: HttpMethod;
};
export type Request = BaseRequest & {
id: string;
timestamp: number;
gqlBody?: GQLRequestBody | undefined;
jwt?: any | undefined;
};
export type Response = {
rawHeaders?: RawHeadersInterface;
status?: number;
body?: any;
timestamp?: number;
};
export type LogEntry = {
originalRequest: Request;
labels?: string[] | undefined;
mockRuleId?: string;
originalResponse?: Response;
modifiedRequest?: Request;
modifiedResponse?: Response;
};
export type RuleMatchFunction = (request: Request) => RuleMatchResult;
export type RequestManipulationFn = (request: Request) => Request | Promise<Request>;
export type ResponseManipulationFn = (request: Request, response: Response) => Response | Promise<Response>;
export type ResponseGenerationFn = (request: Request) => Response | Promise<Response>;
export type Actions =
| {
proxyPass: true;
mockResponse?: undefined;
modifyRequest?: undefined;
modifyResponse?: undefined;
}
| {
mockResponse: Response | ResponseGenerationFn;
proxyPass?: undefined;
modifyRequest?: undefined;
modifyResponse?: undefined;
}
| {
modifyRequest: RequestManipulationFn;
modifyResponse?: ResponseManipulationFn;
proxyPass?: true | undefined;
mockResponse?: undefined;
}
| {
modifyRequest?: RequestManipulationFn;
modifyResponse: ResponseManipulationFn;
proxyPass?: true | undefined;
mockResponse?: undefined;
};
export type Rule = {
id: string;
priority?: number;
matches: RuleMatchFunction; // function for picking request to process
labels?: string[]; // groupping req/res pairs for searching later e.g. ['exoclick', 'testId123']
actions: Actions;
disableAfterUse?: boolean | number; // disable after rule is triggered n-times
removeAfterUse?: boolean | number; // disable after rule is triggered n-times
ttlSeconds: number;
storeTraffic?: boolean;
isEnabled?: boolean;
};
export type DeployedRule = Omit<Rule, 'disableAfterUse' | 'removeAfterUse' | 'ttlSeconds'>;
export type SerializableRule = WithRemotableFunctions<Rule>;
export type SerializedRule = WithSerializedFunctions<SerializableRule>;
export type LiveRule = Rule & {
counter: number;
createdTimestamp: number;
};
export type GQLRequestBody = {
operationName: string;
variables?: any;
query: string;
type: 'query' | 'mutation';
methodName?: string;
};
export type WebGuiConfig = {
disabled: boolean;
};
export type ApiConfig =
| { disabled: true }
| {
port: number;
disabled: false;
apiKeyReadWrite: string | null;
apiKeyReadOnly: string | null;
};
export type ClientConfig = {
protocol: 'http' | 'https';
host: string;
port: number;
timeout: number;
apiKey?: string;
};
export type MockConfig = {
domain: string;
port: number;
httpsPort?: number;
httpsKey?: string;
httpsCert?: string;
timeout: number;
externalDns: string[];
rulesPath: string;
disableProxy?: boolean;
};
export type StorageConfig = {
limitCount: number;
limitSize: number;
ttl: number;
};
export type Config = {
webgui: WebGuiConfig;
api: ApiConfig;
mock: MockConfig;
storage: {
traffic: StorageConfig;
};
client: ClientConfig;
};
export interface RuleExecutorInterface {
addRule: (rule: Rule, overwrite?: boolean) => Promise<LiveRule>;
removeRule: (id: string) => Promise<void>;
enableRule: (id: string) => void;
disableRule: (id: string) => void;
findMatchingRule: (request: Request) => Promise<LiveRule | null>;
getRules: () => Promise<readonly LiveRule[]>;
getRule: (id: string) => Promise<LiveRule | undefined>;
}