@stuntman/shared 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/appError.d.ts +14 -0
- package/dist/appError.js +17 -0
- package/dist/config.d.ts +2 -0
- package/dist/config.js +40 -0
- package/dist/constants.d.ts +14 -0
- package/dist/constants.js +17 -0
- package/dist/index.d.ts +153 -0
- package/dist/index.js +40 -0
- package/dist/logger.d.ts +9 -0
- package/dist/logger.js +17 -0
- package/dist/rawHeaders.d.ts +11 -0
- package/dist/rawHeaders.js +70 -0
- package/dist/stringify.d.ts +1 -0
- package/dist/stringify.js +10 -0
- package/package.json +46 -0
- package/src/appError.ts +30 -0
- package/src/config.ts +48 -0
- package/src/constants.ts +14 -0
- package/src/index.ts +199 -0
- package/src/logger.ts +11 -0
- package/src/rawHeaders.ts +75 -0
- package/src/stringify.ts +7 -0
- package/tsconfig.json +16 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023 Andrzej Pasterczyk
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type * as Stuntman from '.';
|
|
2
|
+
export interface AppErrorInterface {
|
|
3
|
+
name?: string;
|
|
4
|
+
httpCode: Stuntman.HttpCode;
|
|
5
|
+
message: string;
|
|
6
|
+
isOperational?: boolean;
|
|
7
|
+
}
|
|
8
|
+
export declare class AppError extends Error {
|
|
9
|
+
readonly name: string;
|
|
10
|
+
readonly httpCode: Stuntman.HttpCode;
|
|
11
|
+
readonly uuid?: string;
|
|
12
|
+
readonly isOperational: boolean;
|
|
13
|
+
constructor(args: AppErrorInterface);
|
|
14
|
+
}
|
package/dist/appError.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AppError = void 0;
|
|
4
|
+
class AppError extends Error {
|
|
5
|
+
constructor(args) {
|
|
6
|
+
super(args.message);
|
|
7
|
+
this.isOperational = true;
|
|
8
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
9
|
+
this.name = args.name || 'Error';
|
|
10
|
+
this.httpCode = args.httpCode;
|
|
11
|
+
if (args.isOperational !== undefined) {
|
|
12
|
+
this.isOperational = args.isOperational;
|
|
13
|
+
}
|
|
14
|
+
Error.captureStackTrace(this);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
exports.AppError = AppError;
|
package/dist/config.d.ts
ADDED
package/dist/config.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.serverConfig = void 0;
|
|
7
|
+
const _1 = require(".");
|
|
8
|
+
const config_1 = __importDefault(require("config"));
|
|
9
|
+
const defaults_1 = __importDefault(require("defaults"));
|
|
10
|
+
// TODO safeguards & defaults
|
|
11
|
+
const defaultConfig = {
|
|
12
|
+
api: {
|
|
13
|
+
disabled: false,
|
|
14
|
+
port: _1.DEFAULT_API_PORT,
|
|
15
|
+
},
|
|
16
|
+
mock: {
|
|
17
|
+
domain: _1.DEFAULT_MOCK_DOMAIN,
|
|
18
|
+
externalDns: _1.EXTERNAL_DNS,
|
|
19
|
+
port: _1.DEFAULT_MOCK_PORT,
|
|
20
|
+
timeout: _1.DEFAULT_PROXY_TIMEOUT,
|
|
21
|
+
},
|
|
22
|
+
storage: {
|
|
23
|
+
traffic: {
|
|
24
|
+
limitCount: _1.DEFAULT_CACHE_MAX_ENTRIES,
|
|
25
|
+
limitSize: _1.DEFAULT_CACHE_MAX_SIZE,
|
|
26
|
+
ttl: _1.DEFAULT_CACHE_TTL
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
webgui: {
|
|
30
|
+
disabled: false,
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
let configFromFile = {};
|
|
34
|
+
try {
|
|
35
|
+
configFromFile = config_1.default.get('stuntman');
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
console.warn('unable to find correct config');
|
|
39
|
+
}
|
|
40
|
+
exports.serverConfig = (0, defaults_1.default)(configFromFile, defaultConfig);
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export declare const DEFAULT_PROXY_TIMEOUT = 60000;
|
|
2
|
+
export declare const DEFAULT_RULE_PRIORITY = 100;
|
|
3
|
+
export declare const DEFAULT_MOCK_PORT = 2015;
|
|
4
|
+
export declare const DEFAULT_API_PORT = 1985;
|
|
5
|
+
export declare const DEFAULT_CACHE_MAX_ENTRIES = 500;
|
|
6
|
+
export declare const DEFAULT_CACHE_MAX_SIZE: number;
|
|
7
|
+
export declare const DEFAULT_CACHE_TTL: number;
|
|
8
|
+
export declare const DEFAULT_MOCK_DOMAIN = "stuntman";
|
|
9
|
+
export declare const DEFAULT_RULE_TTL_SECONDS: number;
|
|
10
|
+
export declare const MIN_RULE_TTL_SECONDS = 10;
|
|
11
|
+
export declare const MAX_RULE_TTL_SECONDS: number;
|
|
12
|
+
export declare const CATCH_ALL_RULE_PRIORITY: number;
|
|
13
|
+
export declare const CATCH_RULE_NAME = "internal/catch-all";
|
|
14
|
+
export declare const EXTERNAL_DNS: string[];
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.EXTERNAL_DNS = exports.CATCH_RULE_NAME = exports.CATCH_ALL_RULE_PRIORITY = exports.MAX_RULE_TTL_SECONDS = exports.MIN_RULE_TTL_SECONDS = exports.DEFAULT_RULE_TTL_SECONDS = exports.DEFAULT_MOCK_DOMAIN = exports.DEFAULT_CACHE_TTL = exports.DEFAULT_CACHE_MAX_SIZE = exports.DEFAULT_CACHE_MAX_ENTRIES = exports.DEFAULT_API_PORT = exports.DEFAULT_MOCK_PORT = exports.DEFAULT_RULE_PRIORITY = exports.DEFAULT_PROXY_TIMEOUT = void 0;
|
|
4
|
+
exports.DEFAULT_PROXY_TIMEOUT = 60000;
|
|
5
|
+
exports.DEFAULT_RULE_PRIORITY = 100;
|
|
6
|
+
exports.DEFAULT_MOCK_PORT = 2015;
|
|
7
|
+
exports.DEFAULT_API_PORT = 1985;
|
|
8
|
+
exports.DEFAULT_CACHE_MAX_ENTRIES = 500;
|
|
9
|
+
exports.DEFAULT_CACHE_MAX_SIZE = 500 * 1024 * 1024;
|
|
10
|
+
exports.DEFAULT_CACHE_TTL = 1000 * 60 * 60;
|
|
11
|
+
exports.DEFAULT_MOCK_DOMAIN = 'stuntman';
|
|
12
|
+
exports.DEFAULT_RULE_TTL_SECONDS = 60 * 10;
|
|
13
|
+
exports.MIN_RULE_TTL_SECONDS = 10;
|
|
14
|
+
exports.MAX_RULE_TTL_SECONDS = 60 * 60;
|
|
15
|
+
exports.CATCH_ALL_RULE_PRIORITY = Infinity;
|
|
16
|
+
exports.CATCH_RULE_NAME = 'internal/catch-all';
|
|
17
|
+
exports.EXTERNAL_DNS = ['8.8.8.8', '1.1.1.1'];
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
export * from './constants';
|
|
2
|
+
export * from './appError';
|
|
3
|
+
export * from './logger';
|
|
4
|
+
export * from './stringify';
|
|
5
|
+
export * from './rawHeaders';
|
|
6
|
+
export * from './config';
|
|
7
|
+
export declare const INDEX_DTS: string;
|
|
8
|
+
type NonObject = string | number | boolean | symbol | undefined | null | any[];
|
|
9
|
+
interface SerializableTypesRecord<T> {
|
|
10
|
+
[k: string | number]: T;
|
|
11
|
+
}
|
|
12
|
+
export type RecursivePartial<T> = {
|
|
13
|
+
[P in keyof T]?: T[P] extends (infer U)[] ? RecursivePartial<U>[] : T[P] extends object ? RecursivePartial<T[P]> : T[P];
|
|
14
|
+
};
|
|
15
|
+
type SerializableTypes = string | number | boolean | undefined | null | RegExp | SerializableTypes[] | SerializableTypesRecord<SerializableTypes>;
|
|
16
|
+
export declare enum HttpCode {
|
|
17
|
+
OK = 200,
|
|
18
|
+
NO_CONTENT = 204,
|
|
19
|
+
BAD_REQUEST = 400,
|
|
20
|
+
UNAUTHORIZED = 401,
|
|
21
|
+
NOT_FOUND = 404,
|
|
22
|
+
CONFLICT = 409,
|
|
23
|
+
UNPROCESSABLE_ENTITY = 422,
|
|
24
|
+
INTERNAL_SERVER_ERROR = 500
|
|
25
|
+
}
|
|
26
|
+
export type LocalVariables = Record<string, SerializableTypes>;
|
|
27
|
+
export type RuleMatchResult = boolean | {
|
|
28
|
+
result: boolean;
|
|
29
|
+
enableRuleIds?: string[];
|
|
30
|
+
disableRuleIds?: string[];
|
|
31
|
+
};
|
|
32
|
+
export type RemotableFunction<T extends Function> = {
|
|
33
|
+
localFn: T;
|
|
34
|
+
localVariables?: LocalVariables;
|
|
35
|
+
};
|
|
36
|
+
export type SerializedRemotableFunction = {
|
|
37
|
+
localFn: string;
|
|
38
|
+
variable?: string;
|
|
39
|
+
remoteFn: string;
|
|
40
|
+
};
|
|
41
|
+
type WithRemotableFunctions<Type> = {
|
|
42
|
+
[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>>;
|
|
43
|
+
};
|
|
44
|
+
export type WithSerializedFunctions<Type> = {
|
|
45
|
+
[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;
|
|
46
|
+
};
|
|
47
|
+
export interface RawHeadersInterface extends Array<string> {
|
|
48
|
+
get: (name: string) => string | undefined;
|
|
49
|
+
set: (name: string, value: string) => void;
|
|
50
|
+
has: (name: string, value?: string) => boolean;
|
|
51
|
+
add: (name: string, value: string) => void;
|
|
52
|
+
remove: (name: string) => void;
|
|
53
|
+
toHeaderPairs: () => readonly [string, string][];
|
|
54
|
+
}
|
|
55
|
+
export type BaseRequest = {
|
|
56
|
+
rawHeaders: RawHeadersInterface;
|
|
57
|
+
url: string;
|
|
58
|
+
body?: any;
|
|
59
|
+
method: string;
|
|
60
|
+
};
|
|
61
|
+
export type Request = BaseRequest & {
|
|
62
|
+
id: string;
|
|
63
|
+
timestamp: number;
|
|
64
|
+
gqlBody?: GQLRequestBody;
|
|
65
|
+
};
|
|
66
|
+
export type Response = {
|
|
67
|
+
rawHeaders?: RawHeadersInterface;
|
|
68
|
+
status?: number;
|
|
69
|
+
body?: any;
|
|
70
|
+
timestamp?: number;
|
|
71
|
+
};
|
|
72
|
+
export type LogEntry = {
|
|
73
|
+
originalRequest: Request;
|
|
74
|
+
labels?: string[];
|
|
75
|
+
mockRuleId?: string;
|
|
76
|
+
originalResponse?: Response;
|
|
77
|
+
modifiedRequest?: Request;
|
|
78
|
+
modifiedResponse?: Response;
|
|
79
|
+
};
|
|
80
|
+
export type RuleMatchFunction = (request: Request) => RuleMatchResult;
|
|
81
|
+
export type RequestManipulationFn = (request: Request) => Request;
|
|
82
|
+
export type ResponseManipulationFn = (request: Request, response: Response) => Response;
|
|
83
|
+
export type ResponseGenerationFn = (request: Request) => Response;
|
|
84
|
+
export type Actions = {
|
|
85
|
+
mockResponse: Response | ResponseGenerationFn;
|
|
86
|
+
modifyRequest?: undefined;
|
|
87
|
+
modifyResponse?: undefined;
|
|
88
|
+
} | {
|
|
89
|
+
mockResponse?: undefined;
|
|
90
|
+
modifyRequest?: RequestManipulationFn;
|
|
91
|
+
modifyResponse?: ResponseManipulationFn;
|
|
92
|
+
};
|
|
93
|
+
export type Rule = {
|
|
94
|
+
id: string;
|
|
95
|
+
priority?: number;
|
|
96
|
+
matches: RuleMatchFunction;
|
|
97
|
+
labels?: string[];
|
|
98
|
+
actions?: Actions;
|
|
99
|
+
disableAfterUse?: boolean | number;
|
|
100
|
+
removeAfterUse?: boolean | number;
|
|
101
|
+
ttlSeconds: number;
|
|
102
|
+
storeTraffic?: boolean;
|
|
103
|
+
isEnabled?: boolean;
|
|
104
|
+
};
|
|
105
|
+
export type DeployedRule = Omit<Rule, 'disableAfterUse' | 'removeAfterUse' | 'ttlSeconds'>;
|
|
106
|
+
export type SerializableRule = WithRemotableFunctions<Rule>;
|
|
107
|
+
export type SerializedRule = WithSerializedFunctions<SerializableRule>;
|
|
108
|
+
export type LiveRule = Rule & {
|
|
109
|
+
counter: number;
|
|
110
|
+
createdTimestamp: number;
|
|
111
|
+
};
|
|
112
|
+
export type GQLRequestBody = {
|
|
113
|
+
operationName: string;
|
|
114
|
+
variables?: any;
|
|
115
|
+
query: string;
|
|
116
|
+
type: 'query' | 'mutation';
|
|
117
|
+
methodName?: string;
|
|
118
|
+
};
|
|
119
|
+
export type WebGuiConfig = {
|
|
120
|
+
disabled: boolean;
|
|
121
|
+
};
|
|
122
|
+
export type ApiConfig = {
|
|
123
|
+
port: number;
|
|
124
|
+
disabled: boolean;
|
|
125
|
+
};
|
|
126
|
+
export type ClientConfig = {
|
|
127
|
+
protocol?: 'http' | 'https';
|
|
128
|
+
host?: string;
|
|
129
|
+
port?: number;
|
|
130
|
+
timeout?: number;
|
|
131
|
+
};
|
|
132
|
+
export type MockConfig = {
|
|
133
|
+
domain: string;
|
|
134
|
+
port: number;
|
|
135
|
+
httpsPort?: number;
|
|
136
|
+
httpsKey?: string;
|
|
137
|
+
httpsCert?: string;
|
|
138
|
+
timeout: number;
|
|
139
|
+
externalDns: string[];
|
|
140
|
+
};
|
|
141
|
+
export type StorageConfig = {
|
|
142
|
+
limitCount: number;
|
|
143
|
+
limitSize: number;
|
|
144
|
+
ttl: number;
|
|
145
|
+
};
|
|
146
|
+
export type ServerConfig = {
|
|
147
|
+
webgui: WebGuiConfig;
|
|
148
|
+
api: ApiConfig;
|
|
149
|
+
mock: MockConfig;
|
|
150
|
+
storage: {
|
|
151
|
+
traffic: StorageConfig;
|
|
152
|
+
};
|
|
153
|
+
};
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
17
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
18
|
+
};
|
|
19
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
20
|
+
exports.HttpCode = exports.INDEX_DTS = void 0;
|
|
21
|
+
/* eslint-disable @typescript-eslint/ban-types */
|
|
22
|
+
__exportStar(require("./constants"), exports);
|
|
23
|
+
__exportStar(require("./appError"), exports);
|
|
24
|
+
__exportStar(require("./logger"), exports);
|
|
25
|
+
__exportStar(require("./stringify"), exports);
|
|
26
|
+
__exportStar(require("./rawHeaders"), exports);
|
|
27
|
+
__exportStar(require("./config"), exports);
|
|
28
|
+
const fs_1 = __importDefault(require("fs"));
|
|
29
|
+
exports.INDEX_DTS = fs_1.default.readFileSync(`${__dirname}/index.d.ts`, 'utf-8');
|
|
30
|
+
var HttpCode;
|
|
31
|
+
(function (HttpCode) {
|
|
32
|
+
HttpCode[HttpCode["OK"] = 200] = "OK";
|
|
33
|
+
HttpCode[HttpCode["NO_CONTENT"] = 204] = "NO_CONTENT";
|
|
34
|
+
HttpCode[HttpCode["BAD_REQUEST"] = 400] = "BAD_REQUEST";
|
|
35
|
+
HttpCode[HttpCode["UNAUTHORIZED"] = 401] = "UNAUTHORIZED";
|
|
36
|
+
HttpCode[HttpCode["NOT_FOUND"] = 404] = "NOT_FOUND";
|
|
37
|
+
HttpCode[HttpCode["CONFLICT"] = 409] = "CONFLICT";
|
|
38
|
+
HttpCode[HttpCode["UNPROCESSABLE_ENTITY"] = 422] = "UNPROCESSABLE_ENTITY";
|
|
39
|
+
HttpCode[HttpCode["INTERNAL_SERVER_ERROR"] = 500] = "INTERNAL_SERVER_ERROR";
|
|
40
|
+
})(HttpCode = exports.HttpCode || (exports.HttpCode = {}));
|
package/dist/logger.d.ts
ADDED
package/dist/logger.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
var _a;
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.logger = void 0;
|
|
8
|
+
const pino_1 = __importDefault(require("pino"));
|
|
9
|
+
exports.logger = (0, pino_1.default)({
|
|
10
|
+
level: (_a = process.env.LOG_LEVEL) !== null && _a !== void 0 ? _a : 'info',
|
|
11
|
+
messageKey: 'message',
|
|
12
|
+
formatters: {
|
|
13
|
+
level(label) {
|
|
14
|
+
return { level: label };
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type * as Stuntman from '.';
|
|
2
|
+
export declare class RawHeaders extends Array<string> implements Stuntman.RawHeadersInterface {
|
|
3
|
+
get(name: string): string | undefined;
|
|
4
|
+
has(name: string, value?: string): boolean;
|
|
5
|
+
set(name: string, value: string): void;
|
|
6
|
+
add(name: string, value: string): void;
|
|
7
|
+
remove(name: string): void;
|
|
8
|
+
toHeaderPairs(): readonly [string, string][];
|
|
9
|
+
static fromHeaderPairs(headerPairs: [string, string][]): RawHeaders;
|
|
10
|
+
static toHeaderPairs(rawHeaders: string[]): readonly [string, string][];
|
|
11
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.RawHeaders = void 0;
|
|
4
|
+
class RawHeaders extends Array {
|
|
5
|
+
get(name) {
|
|
6
|
+
const headers = this.toHeaderPairs();
|
|
7
|
+
const matchingHeaders = headers.filter((h) => h[0].toLowerCase() === name.toLowerCase());
|
|
8
|
+
if (matchingHeaders.length === 0) {
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
if (matchingHeaders.length === 1) {
|
|
12
|
+
return matchingHeaders[0][1];
|
|
13
|
+
}
|
|
14
|
+
throw new Error('Multiple headers with same name. Manipulate rawHeaders instead');
|
|
15
|
+
}
|
|
16
|
+
has(name, value) {
|
|
17
|
+
const foundValue = this.get(name);
|
|
18
|
+
if (value === undefined) {
|
|
19
|
+
return foundValue !== undefined;
|
|
20
|
+
}
|
|
21
|
+
return foundValue === value;
|
|
22
|
+
}
|
|
23
|
+
set(name, value) {
|
|
24
|
+
let foundHeaders = 0;
|
|
25
|
+
for (let headerIndex = 0; headerIndex < this.length; headerIndex += 2) {
|
|
26
|
+
if (this[headerIndex].toLowerCase() === name.toLowerCase()) {
|
|
27
|
+
this[headerIndex + 1] = value;
|
|
28
|
+
++foundHeaders;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
if (foundHeaders === 0) {
|
|
32
|
+
return this.add(name, value);
|
|
33
|
+
}
|
|
34
|
+
if (foundHeaders > 1) {
|
|
35
|
+
throw new Error('Multiple headers with same name. Manipulate rawHeaders instead');
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
add(name, value) {
|
|
39
|
+
this.push(name);
|
|
40
|
+
this.push(value);
|
|
41
|
+
}
|
|
42
|
+
remove(name) {
|
|
43
|
+
const headersCopy = [...this];
|
|
44
|
+
let foundHeaders = 0;
|
|
45
|
+
for (let headerIndex = 0; headerIndex < headersCopy.length; headerIndex += 2) {
|
|
46
|
+
if (this[headerIndex - foundHeaders * 2].toLowerCase() === name.toLowerCase()) {
|
|
47
|
+
delete this[headerIndex];
|
|
48
|
+
delete this[headerIndex];
|
|
49
|
+
++foundHeaders;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (foundHeaders > 1) {
|
|
53
|
+
throw new Error('Multiple headers with same name. Manipulate rawHeaders instead');
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
toHeaderPairs() {
|
|
57
|
+
return RawHeaders.toHeaderPairs(this);
|
|
58
|
+
}
|
|
59
|
+
static fromHeaderPairs(headerPairs) {
|
|
60
|
+
return new RawHeaders(...headerPairs.flatMap((x) => x));
|
|
61
|
+
}
|
|
62
|
+
static toHeaderPairs(rawHeaders) {
|
|
63
|
+
const headers = new Array();
|
|
64
|
+
for (let headerIndex = 0; headerIndex < rawHeaders.length; headerIndex += 2) {
|
|
65
|
+
headers.push([rawHeaders[headerIndex], rawHeaders[headerIndex + 1]]);
|
|
66
|
+
}
|
|
67
|
+
return headers;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
exports.RawHeaders = RawHeaders;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const stringify: (obj: any) => string;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.stringify = void 0;
|
|
4
|
+
const stringify = (obj) => JSON.stringify(obj, (key, value) => {
|
|
5
|
+
if (typeof value === 'function' || value instanceof RegExp) {
|
|
6
|
+
return value.toString();
|
|
7
|
+
}
|
|
8
|
+
return value;
|
|
9
|
+
});
|
|
10
|
+
exports.stringify = stringify;
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@stuntman/shared",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Stuntman - HTTP proxy / mock shared types and utils",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/andrzej-woof/stuntman.git"
|
|
9
|
+
},
|
|
10
|
+
"keywords": [
|
|
11
|
+
"proxy",
|
|
12
|
+
"mock",
|
|
13
|
+
"http",
|
|
14
|
+
"https",
|
|
15
|
+
"server",
|
|
16
|
+
"api",
|
|
17
|
+
"e2e",
|
|
18
|
+
"development",
|
|
19
|
+
"rest",
|
|
20
|
+
"gql",
|
|
21
|
+
"end-to-end",
|
|
22
|
+
"testing",
|
|
23
|
+
"qa",
|
|
24
|
+
"automated-testing",
|
|
25
|
+
"stub",
|
|
26
|
+
"functional"
|
|
27
|
+
],
|
|
28
|
+
"author": "Andrzej Pasterczyk",
|
|
29
|
+
"license": "MIT",
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"config": "3.3.9",
|
|
32
|
+
"defaults": "1.0.4",
|
|
33
|
+
"pino": "8.10.0"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@types/config": "3.3.0",
|
|
37
|
+
"@types/defaults": "1.0.3"
|
|
38
|
+
},
|
|
39
|
+
"scripts": {
|
|
40
|
+
"test": "echo \"Error: no test specified\" && exit 1",
|
|
41
|
+
"clean": "rm -fr dist",
|
|
42
|
+
"build": "tsc",
|
|
43
|
+
"lint": "prettier --check . && eslint . --ext ts",
|
|
44
|
+
"lint:fix": "prettier --write ./src && eslint ./src --ext ts --fix"
|
|
45
|
+
}
|
|
46
|
+
}
|
package/src/appError.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type * as Stuntman from '.';
|
|
2
|
+
|
|
3
|
+
export interface AppErrorInterface {
|
|
4
|
+
name?: string;
|
|
5
|
+
httpCode: Stuntman.HttpCode;
|
|
6
|
+
message: string;
|
|
7
|
+
isOperational?: boolean;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export class AppError extends Error {
|
|
11
|
+
public readonly name: string;
|
|
12
|
+
public readonly httpCode: Stuntman.HttpCode;
|
|
13
|
+
public readonly uuid?: string;
|
|
14
|
+
public readonly isOperational: boolean = true;
|
|
15
|
+
|
|
16
|
+
constructor(args: AppErrorInterface) {
|
|
17
|
+
super(args.message);
|
|
18
|
+
|
|
19
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
20
|
+
|
|
21
|
+
this.name = args.name || 'Error';
|
|
22
|
+
this.httpCode = args.httpCode;
|
|
23
|
+
|
|
24
|
+
if (args.isOperational !== undefined) {
|
|
25
|
+
this.isOperational = args.isOperational;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
Error.captureStackTrace(this);
|
|
29
|
+
}
|
|
30
|
+
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import {
|
|
2
|
+
RecursivePartial,
|
|
3
|
+
ServerConfig,
|
|
4
|
+
DEFAULT_API_PORT,
|
|
5
|
+
DEFAULT_MOCK_DOMAIN,
|
|
6
|
+
EXTERNAL_DNS,
|
|
7
|
+
DEFAULT_MOCK_PORT,
|
|
8
|
+
DEFAULT_PROXY_TIMEOUT,
|
|
9
|
+
DEFAULT_CACHE_MAX_ENTRIES,
|
|
10
|
+
DEFAULT_CACHE_MAX_SIZE,
|
|
11
|
+
DEFAULT_CACHE_TTL,
|
|
12
|
+
} from '.';
|
|
13
|
+
import config from 'config';
|
|
14
|
+
import defaults from 'defaults';
|
|
15
|
+
|
|
16
|
+
// TODO safeguards & defaults
|
|
17
|
+
|
|
18
|
+
const defaultConfig: ServerConfig = {
|
|
19
|
+
api: {
|
|
20
|
+
disabled: false,
|
|
21
|
+
port: DEFAULT_API_PORT,
|
|
22
|
+
},
|
|
23
|
+
mock: {
|
|
24
|
+
domain: DEFAULT_MOCK_DOMAIN,
|
|
25
|
+
externalDns: EXTERNAL_DNS,
|
|
26
|
+
port: DEFAULT_MOCK_PORT,
|
|
27
|
+
timeout: DEFAULT_PROXY_TIMEOUT,
|
|
28
|
+
},
|
|
29
|
+
storage: {
|
|
30
|
+
traffic: {
|
|
31
|
+
limitCount: DEFAULT_CACHE_MAX_ENTRIES,
|
|
32
|
+
limitSize: DEFAULT_CACHE_MAX_SIZE,
|
|
33
|
+
ttl: DEFAULT_CACHE_TTL
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
webgui: {
|
|
37
|
+
disabled: false,
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
let configFromFile: RecursivePartial<ServerConfig> = {};
|
|
42
|
+
try {
|
|
43
|
+
configFromFile = config.get<RecursivePartial<ServerConfig>>('stuntman');
|
|
44
|
+
} catch (error) {
|
|
45
|
+
console.warn('unable to find correct config');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export const serverConfig = defaults(configFromFile, defaultConfig) as ServerConfig;
|
package/src/constants.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export const DEFAULT_PROXY_TIMEOUT = 60000;
|
|
2
|
+
export const DEFAULT_RULE_PRIORITY = 100;
|
|
3
|
+
export const DEFAULT_MOCK_PORT = 2015;
|
|
4
|
+
export const DEFAULT_API_PORT = 1985;
|
|
5
|
+
export const DEFAULT_CACHE_MAX_ENTRIES = 500;
|
|
6
|
+
export const DEFAULT_CACHE_MAX_SIZE = 500 * 1024 * 1024;
|
|
7
|
+
export const DEFAULT_CACHE_TTL = 1000 * 60 * 60;
|
|
8
|
+
export const DEFAULT_MOCK_DOMAIN = 'stuntman';
|
|
9
|
+
export const DEFAULT_RULE_TTL_SECONDS = 60 * 10;
|
|
10
|
+
export const MIN_RULE_TTL_SECONDS = 10;
|
|
11
|
+
export const MAX_RULE_TTL_SECONDS = 60 * 60;
|
|
12
|
+
export const CATCH_ALL_RULE_PRIORITY = Infinity;
|
|
13
|
+
export const CATCH_RULE_NAME = 'internal/catch-all';
|
|
14
|
+
export const EXTERNAL_DNS = ['8.8.8.8', '1.1.1.1'];
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/ban-types */
|
|
2
|
+
export * from './constants';
|
|
3
|
+
export * from './appError';
|
|
4
|
+
export * from './logger';
|
|
5
|
+
export * from './stringify';
|
|
6
|
+
export * from './rawHeaders';
|
|
7
|
+
export * from './config';
|
|
8
|
+
|
|
9
|
+
import fs from 'fs';
|
|
10
|
+
export const INDEX_DTS = fs.readFileSync(`${__dirname}/index.d.ts`, 'utf-8');
|
|
11
|
+
|
|
12
|
+
type NonObject = string | number | boolean | symbol | undefined | null | any[];
|
|
13
|
+
|
|
14
|
+
interface SerializableTypesRecord<T> {
|
|
15
|
+
[k: string | number]: T;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export type RecursivePartial<T> = {
|
|
19
|
+
[P in keyof T]?: T[P] extends (infer U)[] ? RecursivePartial<U>[] : T[P] extends object ? RecursivePartial<T[P]> : T[P];
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
type SerializableTypes =
|
|
23
|
+
| string
|
|
24
|
+
| number
|
|
25
|
+
| boolean
|
|
26
|
+
| undefined
|
|
27
|
+
| null
|
|
28
|
+
| RegExp
|
|
29
|
+
| SerializableTypes[]
|
|
30
|
+
| SerializableTypesRecord<SerializableTypes>;
|
|
31
|
+
|
|
32
|
+
export enum HttpCode {
|
|
33
|
+
OK = 200,
|
|
34
|
+
NO_CONTENT = 204,
|
|
35
|
+
BAD_REQUEST = 400,
|
|
36
|
+
UNAUTHORIZED = 401,
|
|
37
|
+
NOT_FOUND = 404,
|
|
38
|
+
CONFLICT = 409,
|
|
39
|
+
UNPROCESSABLE_ENTITY = 422,
|
|
40
|
+
INTERNAL_SERVER_ERROR = 500,
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export type LocalVariables = Record<string, SerializableTypes>;
|
|
44
|
+
|
|
45
|
+
export type RuleMatchResult = boolean | { result: boolean; enableRuleIds?: string[]; disableRuleIds?: string[] };
|
|
46
|
+
|
|
47
|
+
export type RemotableFunction<T extends Function> = {
|
|
48
|
+
localFn: T;
|
|
49
|
+
localVariables?: LocalVariables;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export type SerializedRemotableFunction = {
|
|
53
|
+
localFn: string;
|
|
54
|
+
variable?: string;
|
|
55
|
+
remoteFn: string;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
type WithRemotableFunctions<Type> = {
|
|
59
|
+
[PropertyKey in keyof Type]: Extract<Type[PropertyKey], Function> extends never
|
|
60
|
+
? Exclude<Type[PropertyKey], NonObject> extends never
|
|
61
|
+
? Type[PropertyKey]
|
|
62
|
+
: WithRemotableFunctions<Exclude<Type[PropertyKey], NonObject>>
|
|
63
|
+
: Exclude<Type[PropertyKey], Function> | RemotableFunction<Extract<Type[PropertyKey], Function>>;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export type WithSerializedFunctions<Type> = {
|
|
67
|
+
[PropertyKey in keyof Type]: Extract<Type[PropertyKey], RemotableFunction<Function>> extends never
|
|
68
|
+
? Exclude<Type[PropertyKey], NonObject> extends never
|
|
69
|
+
? Type[PropertyKey]
|
|
70
|
+
: WithSerializedFunctions<Exclude<Type[PropertyKey], NonObject>>
|
|
71
|
+
: Exclude<Type[PropertyKey], RemotableFunction<Function>> | SerializedRemotableFunction;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export interface RawHeadersInterface extends Array<string> {
|
|
75
|
+
get: (name: string) => string | undefined;
|
|
76
|
+
set: (name: string, value: string) => void;
|
|
77
|
+
has: (name: string, value?: string) => boolean;
|
|
78
|
+
add: (name: string, value: string) => void;
|
|
79
|
+
remove: (name: string) => void;
|
|
80
|
+
toHeaderPairs: () => readonly [string, string][];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export type BaseRequest = {
|
|
84
|
+
rawHeaders: RawHeadersInterface;
|
|
85
|
+
url: string;
|
|
86
|
+
body?: any;
|
|
87
|
+
method: string;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
export type Request = BaseRequest & {
|
|
91
|
+
id: string;
|
|
92
|
+
timestamp: number;
|
|
93
|
+
gqlBody?: GQLRequestBody;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
export type Response = {
|
|
97
|
+
rawHeaders?: RawHeadersInterface;
|
|
98
|
+
status?: number;
|
|
99
|
+
body?: any;
|
|
100
|
+
timestamp?: number;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
export type LogEntry = {
|
|
104
|
+
originalRequest: Request;
|
|
105
|
+
labels?: string[];
|
|
106
|
+
mockRuleId?: string;
|
|
107
|
+
originalResponse?: Response;
|
|
108
|
+
modifiedRequest?: Request;
|
|
109
|
+
modifiedResponse?: Response;
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
export type RuleMatchFunction = (request: Request) => RuleMatchResult;
|
|
113
|
+
export type RequestManipulationFn = (request: Request) => Request;
|
|
114
|
+
export type ResponseManipulationFn = (request: Request, response: Response) => Response;
|
|
115
|
+
export type ResponseGenerationFn = (request: Request) => Response;
|
|
116
|
+
|
|
117
|
+
export type Actions =
|
|
118
|
+
| {
|
|
119
|
+
mockResponse: Response | ResponseGenerationFn;
|
|
120
|
+
modifyRequest?: undefined;
|
|
121
|
+
modifyResponse?: undefined;
|
|
122
|
+
}
|
|
123
|
+
| {
|
|
124
|
+
mockResponse?: undefined;
|
|
125
|
+
modifyRequest?: RequestManipulationFn;
|
|
126
|
+
modifyResponse?: ResponseManipulationFn;
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
export type Rule = {
|
|
130
|
+
id: string;
|
|
131
|
+
priority?: number;
|
|
132
|
+
matches: RuleMatchFunction; // function for picking request to process
|
|
133
|
+
labels?: string[]; // groupping req/res pairs for searching later e.g. ['exoclick', 'testId123']
|
|
134
|
+
actions?: Actions;
|
|
135
|
+
disableAfterUse?: boolean | number; // disable after rule is triggered n-times
|
|
136
|
+
removeAfterUse?: boolean | number; // disable after rule is triggered n-times
|
|
137
|
+
ttlSeconds: number;
|
|
138
|
+
storeTraffic?: boolean;
|
|
139
|
+
isEnabled?: boolean;
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
export type DeployedRule = Omit<Rule, 'disableAfterUse' | 'removeAfterUse' | 'ttlSeconds'>;
|
|
143
|
+
|
|
144
|
+
export type SerializableRule = WithRemotableFunctions<Rule>;
|
|
145
|
+
export type SerializedRule = WithSerializedFunctions<SerializableRule>;
|
|
146
|
+
|
|
147
|
+
export type LiveRule = Rule & {
|
|
148
|
+
counter: number;
|
|
149
|
+
createdTimestamp: number;
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
export type GQLRequestBody = {
|
|
153
|
+
operationName: string;
|
|
154
|
+
variables?: any;
|
|
155
|
+
query: string;
|
|
156
|
+
type: 'query' | 'mutation';
|
|
157
|
+
methodName?: string;
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
export type WebGuiConfig = {
|
|
161
|
+
disabled: boolean;
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
export type ApiConfig = {
|
|
165
|
+
port: number;
|
|
166
|
+
disabled: boolean;
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
export type ClientConfig = {
|
|
170
|
+
protocol?: 'http' | 'https';
|
|
171
|
+
host?: string;
|
|
172
|
+
port?: number;
|
|
173
|
+
timeout?: number;
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
export type MockConfig = {
|
|
177
|
+
domain: string;
|
|
178
|
+
port: number;
|
|
179
|
+
httpsPort?: number;
|
|
180
|
+
httpsKey?: string;
|
|
181
|
+
httpsCert?: string;
|
|
182
|
+
timeout: number;
|
|
183
|
+
externalDns: string[];
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
export type StorageConfig = {
|
|
187
|
+
limitCount: number;
|
|
188
|
+
limitSize: number;
|
|
189
|
+
ttl: number;
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
export type ServerConfig = {
|
|
193
|
+
webgui: WebGuiConfig;
|
|
194
|
+
api: ApiConfig;
|
|
195
|
+
mock: MockConfig;
|
|
196
|
+
storage: {
|
|
197
|
+
traffic: StorageConfig;
|
|
198
|
+
};
|
|
199
|
+
};
|
package/src/logger.ts
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type * as Stuntman from '.';
|
|
2
|
+
|
|
3
|
+
export class RawHeaders extends Array<string> implements Stuntman.RawHeadersInterface {
|
|
4
|
+
get(name: string): string | undefined {
|
|
5
|
+
const headers = this.toHeaderPairs();
|
|
6
|
+
const matchingHeaders = headers.filter((h) => h[0].toLowerCase() === name.toLowerCase());
|
|
7
|
+
if (matchingHeaders.length === 0) {
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
if (matchingHeaders.length === 1) {
|
|
11
|
+
return matchingHeaders[0][1];
|
|
12
|
+
}
|
|
13
|
+
throw new Error('Multiple headers with same name. Manipulate rawHeaders instead');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
has(name: string, value?: string): boolean {
|
|
17
|
+
const foundValue = this.get(name);
|
|
18
|
+
if (value === undefined) {
|
|
19
|
+
return foundValue !== undefined;
|
|
20
|
+
}
|
|
21
|
+
return foundValue === value;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
set(name: string, value: string): void {
|
|
25
|
+
let foundHeaders = 0;
|
|
26
|
+
for (let headerIndex = 0; headerIndex < this.length; headerIndex += 2) {
|
|
27
|
+
if (this[headerIndex].toLowerCase() === name.toLowerCase()) {
|
|
28
|
+
this[headerIndex + 1] = value;
|
|
29
|
+
++foundHeaders;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
if (foundHeaders === 0) {
|
|
33
|
+
return this.add(name, value);
|
|
34
|
+
}
|
|
35
|
+
if (foundHeaders > 1) {
|
|
36
|
+
throw new Error('Multiple headers with same name. Manipulate rawHeaders instead');
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
add(name: string, value: string): void {
|
|
41
|
+
this.push(name);
|
|
42
|
+
this.push(value);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
remove(name: string): void {
|
|
46
|
+
const headersCopy = [...this];
|
|
47
|
+
let foundHeaders = 0;
|
|
48
|
+
for (let headerIndex = 0; headerIndex < headersCopy.length; headerIndex += 2) {
|
|
49
|
+
if (this[headerIndex - foundHeaders * 2].toLowerCase() === name.toLowerCase()) {
|
|
50
|
+
delete this[headerIndex];
|
|
51
|
+
delete this[headerIndex];
|
|
52
|
+
++foundHeaders;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (foundHeaders > 1) {
|
|
56
|
+
throw new Error('Multiple headers with same name. Manipulate rawHeaders instead');
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
toHeaderPairs(): readonly [string, string][] {
|
|
61
|
+
return RawHeaders.toHeaderPairs(this);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
static fromHeaderPairs(headerPairs: [string, string][]): RawHeaders {
|
|
65
|
+
return new RawHeaders(...headerPairs.flatMap((x) => x));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
static toHeaderPairs(rawHeaders: string[]): readonly [string, string][] {
|
|
69
|
+
const headers = new Array<[string, string]>();
|
|
70
|
+
for (let headerIndex = 0; headerIndex < rawHeaders.length; headerIndex += 2) {
|
|
71
|
+
headers.push([rawHeaders[headerIndex], rawHeaders[headerIndex + 1]]);
|
|
72
|
+
}
|
|
73
|
+
return headers;
|
|
74
|
+
}
|
|
75
|
+
}
|
package/src/stringify.ts
ADDED
package/tsconfig.json
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2018",
|
|
4
|
+
"module": "commonjs",
|
|
5
|
+
"rootDir": "src",
|
|
6
|
+
"declaration": true,
|
|
7
|
+
"outDir": "dist",
|
|
8
|
+
"esModuleInterop": true,
|
|
9
|
+
"forceConsistentCasingInFileNames": true,
|
|
10
|
+
"strict": true,
|
|
11
|
+
"skipLibCheck": true,
|
|
12
|
+
"typeRoots": ["node_modules/@types"],
|
|
13
|
+
"allowJs": true,
|
|
14
|
+
"moduleResolution": "node16"
|
|
15
|
+
}
|
|
16
|
+
}
|