From b624110a54e87cf3d466106ec56acb9655539542 Mon Sep 17 00:00:00 2001 From: kurpav Date: Thu, 30 Nov 2023 21:17:42 +0200 Subject: [PATCH 01/30] feat: implement basic http interceptor --- package.json | 1 + src/interceptor/index.ts | 0 src/interceptor/utils/createRequest.test.ts | 127 +++++++++++++ src/interceptor/utils/createRequest.ts | 49 ++++++ src/interceptor/utils/createResponse.test.ts | 44 +++++ src/interceptor/utils/createResponse.ts | 57 ++++++ src/utils/cloneObject.test.ts | 93 ++++++++++ src/utils/cloneObject.ts | 36 ++++ src/utils/getRequestOptionsByUrl.ts | 29 +++ src/utils/getUrlByRequestOptions.test.ts | 134 ++++++++++++++ src/utils/getUrlByRequestOptions.ts | 176 +++++++++++++++++++ src/utils/isObject.test.ts | 19 ++ src/utils/isObject.ts | 6 + src/utils/responseUtils.ts | 5 + yarn.lock | 73 ++++++++ 15 files changed, 849 insertions(+) create mode 100644 src/interceptor/index.ts create mode 100644 src/interceptor/utils/createRequest.test.ts create mode 100644 src/interceptor/utils/createRequest.ts create mode 100644 src/interceptor/utils/createResponse.test.ts create mode 100644 src/interceptor/utils/createResponse.ts create mode 100644 src/utils/cloneObject.test.ts create mode 100644 src/utils/cloneObject.ts create mode 100644 src/utils/getRequestOptionsByUrl.ts create mode 100644 src/utils/getUrlByRequestOptions.test.ts create mode 100644 src/utils/getUrlByRequestOptions.ts create mode 100644 src/utils/isObject.test.ts create mode 100644 src/utils/isObject.ts create mode 100644 src/utils/responseUtils.ts diff --git a/package.json b/package.json index f39f089..131da06 100644 --- a/package.json +++ b/package.json @@ -67,6 +67,7 @@ "jest-extended": "^4.0.2", "json-server": "^0.17.0", "openai": "^4.10.0", + "pino-pretty": "^10.2.3", "postgres": "^3.3.4", "prettier": "^2.8.1", "superagent": "^8.0.9", diff --git a/src/interceptor/index.ts b/src/interceptor/index.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/interceptor/utils/createRequest.test.ts b/src/interceptor/utils/createRequest.test.ts new file mode 100644 index 0000000..00ca504 --- /dev/null +++ b/src/interceptor/utils/createRequest.test.ts @@ -0,0 +1,127 @@ +import { NodeClientRequest } from '../NodeClientRequest'; +import { createRequest } from './createRequest'; +import { EventEmitter } from 'events'; + +const emitter = new EventEmitter(); + +it('creates a fetch Request with a JSON body', async () => { + const clientRequest = new NodeClientRequest( + [ + new URL('https://api.github.com'), + { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + } + }, + () => {} + ], + { + emitter + } + ); + clientRequest.write(JSON.stringify({ firstName: 'John' })); + + const request = createRequest(clientRequest); + + expect(request.method).toBe('POST'); + expect(request.url).toBe('https://api.github.com/'); + expect(request.headers.get('Content-Type')).toBe('application/json'); + expect(await request.json()).toEqual({ firstName: 'John' }); +}); + +it('creates a fetch Request with an empty body', async () => { + const clientRequest = new NodeClientRequest( + [ + new URL('https://api.github.com'), + { + method: 'GET', + headers: { + Accept: 'application/json' + } + }, + () => {} + ], + { + emitter + } + ); + + const request = createRequest(clientRequest); + + expect(request.method).toBe('GET'); + expect(request.url).toBe('https://api.github.com/'); + expect(request.headers.get('Accept')).toBe('application/json'); + expect(request.body).toBe(null); +}); + +it('creates a fetch Request with an empty string body', async () => { + const clientRequest = new NodeClientRequest( + [ + new URL('https://api.github.com'), + { + method: 'HEAD' + }, + () => {} + ], + { + emitter + } + ); + clientRequest.write(''); + + const request = createRequest(clientRequest); + + expect(request.method).toBe('HEAD'); + expect(request.url).toBe('https://api.github.com/'); + expect(request.body).toBe(null); +}); + +it('creates a fetch Request with an empty password', async () => { + const clientRequest = new NodeClientRequest( + [new URL('https://api.github.com'), { auth: 'username:' }, () => {}], + { + emitter + } + ); + clientRequest.write(''); + + const request = createRequest(clientRequest); + + expect(request.headers.get('Authorization')).toBe( + `Basic ${btoa('username:')}` + ); + expect(request.url).toBe('https://api.github.com/'); +}); + +it('creates a fetch Request with an empty username', async () => { + const clientRequest = new NodeClientRequest( + [new URL('https://api.github.com'), { auth: ':password' }, () => {}], + { + emitter + } + ); + clientRequest.write(''); + + const request = createRequest(clientRequest); + + expect(request.headers.get('Authorization')).toBe( + `Basic ${btoa(':password')}` + ); + expect(request.url).toBe('https://api.github.com/'); +}); + +it('creates a fetch Request with falsy headers', async () => { + const clientRequest = new NodeClientRequest( + [new URL('https://api.github.com'), { headers: { foo: 0, empty: '' } }], + { + emitter + } + ); + clientRequest.write(''); + + const request = createRequest(clientRequest); + + expect(request.headers.get('foo')).toBe('0'); + expect(request.headers.get('empty')).toBe(''); +}); diff --git a/src/interceptor/utils/createRequest.ts b/src/interceptor/utils/createRequest.ts new file mode 100644 index 0000000..fbc4165 --- /dev/null +++ b/src/interceptor/utils/createRequest.ts @@ -0,0 +1,49 @@ +import type { NodeClientRequest } from '../NodeClientRequest' + +/** + * Creates a Fetch API `Request` instance from the given `http.ClientRequest`. + */ +export function createRequest(clientRequest: NodeClientRequest): Request { + const headers = new Headers() + + const outgoingHeaders = clientRequest.getHeaders() + for (const headerName in outgoingHeaders) { + const headerValue = outgoingHeaders[headerName] + + if (typeof headerValue === 'undefined') { + continue + } + + const valuesList = Array.prototype.concat([], headerValue) + for (const value of valuesList) { + headers.append(headerName, value.toString()) + } + } + + /** + * Translate the authentication from the request URL to + * the request "Authorization" header. + * @see https://github.com/mswjs/interceptors/issues/438 + */ + if (clientRequest.url.username || clientRequest.url.password) { + const auth = `${clientRequest.url.username || ''}:${clientRequest.url.password || ''}` + headers.set('Authorization', `Basic ${btoa(auth)}`) + + // Remove the credentials from the URL since you cannot + // construct a Request instance with such a URL. + clientRequest.url.username = '' + clientRequest.url.password = '' + } + + const method = clientRequest.method || 'GET' + + return new Request(clientRequest.url, { + method, + headers, + credentials: 'same-origin', + body: + method === 'HEAD' || method === 'GET' + ? null + : clientRequest.requestBuffer, + }) +} diff --git a/src/interceptor/utils/createResponse.test.ts b/src/interceptor/utils/createResponse.test.ts new file mode 100644 index 0000000..d0ac7e9 --- /dev/null +++ b/src/interceptor/utils/createResponse.test.ts @@ -0,0 +1,44 @@ +import { Socket } from 'net'; +import * as http from 'http'; +import { createResponse } from './createResponse'; +import { responseStatusCodesWithoutBody } from '../../utils/responseUtils'; + +it('creates a fetch api response from http incoming message', async () => { + const message = new http.IncomingMessage(new Socket()); + message.statusCode = 201; + message.statusMessage = 'Created'; + message.headers['content-type'] = 'application/json'; + + const response = createResponse(message); + + message.emit('data', Buffer.from('{"firstName":')); + message.emit('data', Buffer.from('"John"}')); + message.emit('end'); + + expect(response.status).toBe(201); + expect(response.statusText).toBe('Created'); + expect(response.headers.get('content-type')).toBe('application/json'); + expect(await response.json()).toEqual({ firstName: 'John' }); +}); + +it.each(responseStatusCodesWithoutBody)( + 'ignores message body for %i response status', + (responseStatus) => { + const message = new http.IncomingMessage(new Socket()); + message.statusCode = responseStatus; + + const response = createResponse(message); + + // These chunks will be ignored: this response + // cannot have body. We don't forward this error to + // the consumer because it's us who converts the + // internal stream to a Fetch API Response instance. + // Consumers will rely on the Response API when constructing + // mocked responses. + message.emit('data', Buffer.from('hello')); + message.emit('end'); + + expect(response.status).toBe(responseStatus); + expect(response.body).toBe(null); + } +); diff --git a/src/interceptor/utils/createResponse.ts b/src/interceptor/utils/createResponse.ts new file mode 100644 index 0000000..31af1f7 --- /dev/null +++ b/src/interceptor/utils/createResponse.ts @@ -0,0 +1,57 @@ +import type { IncomingHttpHeaders, IncomingMessage } from 'http' +import { responseStatusCodesWithoutBody } from '../../utils/responseUtils' + +/** + * Creates a Fetch API `Response` instance from the given + * `http.IncomingMessage` instance. + */ +export function createResponse(message: IncomingMessage): Response { + const responseBodyOrNull = responseStatusCodesWithoutBody.includes( + message.statusCode || 200 + ) + ? null + : new ReadableStream({ + start(controller) { + message.on('data', (chunk) => controller.enqueue(chunk)) + message.on('end', () => controller.close()) + + /** + * @todo Should also listen to the "error" on the message + * and forward it to the controller. Otherwise the stream + * will pend indefinitely. + */ + }, + }) + + return new Response(responseBodyOrNull, { + status: message.statusCode, + statusText: message.statusMessage, + headers: createHeadersFromIncomingHttpHeaders(message.headers), + }) +} + +function createHeadersFromIncomingHttpHeaders( + httpHeaders: IncomingHttpHeaders +): Headers { + const headers = new Headers() + + for (const headerName in httpHeaders) { + const headerValues = httpHeaders[headerName] + + if (typeof headerValues === 'undefined') { + continue + } + + if (Array.isArray(headerValues)) { + headerValues.forEach((headerValue) => { + headers.append(headerName, headerValue) + }) + + continue + } + + headers.set(headerName, headerValues) + } + + return headers +} diff --git a/src/utils/cloneObject.test.ts b/src/utils/cloneObject.test.ts new file mode 100644 index 0000000..fdc62a2 --- /dev/null +++ b/src/utils/cloneObject.test.ts @@ -0,0 +1,93 @@ +import { cloneObject } from './cloneObject'; + +it('clones a shallow object', () => { + const original = { a: 1, b: 2, c: [1, 2, 3] }; + const clone = cloneObject(original); + + expect(clone).toEqual(original); + + clone.a = 5; + clone.b = 6; + clone.c = [5, 6, 7]; + + expect(clone).toHaveProperty('a', 5); + expect(clone).toHaveProperty('b', 6); + expect(clone).toHaveProperty('c', [5, 6, 7]); + expect(original).toHaveProperty('a', 1); + expect(original).toHaveProperty('b', 2); + expect(original).toHaveProperty('c', [1, 2, 3]); +}); + +it('clones a nested object', () => { + const original = { a: { b: 1 }, c: { d: { e: 2 } } }; + const clone = cloneObject(original); + + expect(clone).toEqual(original); + + clone.a.b = 10; + clone.c.d.e = 20; + + expect(clone).toHaveProperty(['a', 'b'], 10); + expect(clone).toHaveProperty(['c', 'd', 'e'], 20); + expect(original).toHaveProperty(['a', 'b'], 1); + expect(original).toHaveProperty(['c', 'd', 'e'], 2); +}); + +it('clones a class instance', () => { + class Car { + public manufacturer: string; + constructor() { + this.manufacturer = 'Audi'; + } + getManufacturer() { + return this.manufacturer; + } + } + + const car = new Car(); + const clone = cloneObject(car); + + expect(clone).toHaveProperty('manufacturer', 'Audi'); + expect(clone).toHaveProperty('getManufacturer'); + expect(clone.getManufacturer).toBeInstanceOf(Function); + expect(clone.getManufacturer()).toEqual('Audi'); +}); + +it('ignores nested class instances', () => { + class Car { + name: string; + constructor(name: string) { + this.name = name; + } + getName() { + return this.name; + } + } + const original = { + a: 1, + car: new Car('Audi') + }; + const clone = cloneObject(original); + + expect(clone).toEqual(original); + expect(clone.car).toBeInstanceOf(Car); + expect(clone.car.getName()).toEqual('Audi'); + + clone.car = new Car('BMW'); + + expect(clone.car).toBeInstanceOf(Car); + expect(clone.car.getName()).toEqual('BMW'); + expect(original.car).toBeInstanceOf(Car); + expect(original.car.getName()).toEqual('Audi'); +}); + +it('clones an object with null prototype', () => { + const original = { + key: Object.create(null) + }; + const clone = cloneObject(original); + + expect(clone).toEqual({ + key: {} + }); +}); diff --git a/src/utils/cloneObject.ts b/src/utils/cloneObject.ts new file mode 100644 index 0000000..a70a4c9 --- /dev/null +++ b/src/utils/cloneObject.ts @@ -0,0 +1,36 @@ +import { pinoLogger } from '../logger' + +const logger = pinoLogger.child({ module: 'cloneObject' }) + +function isPlainObject(obj?: Record): boolean { + logger.info('is plain object?', obj) + + if (obj == null || !obj.constructor?.name) { + logger.info('given object is undefined, not a plain object...') + return false + } + + logger.info('checking the object constructor:', obj.constructor.name) + return obj.constructor.name === 'Object' +} + +export function cloneObject>( + obj: ObjectType +): ObjectType { + logger.info('cloning object:', obj) + + const enumerableProperties = Object.entries(obj).reduce>( + (acc, [key, value]) => { + logger.info('analyzing key-value pair:', key, value) + + // Recursively clone only plain objects, omitting class instances. + acc[key] = isPlainObject(value) ? cloneObject(value) : value + return acc + }, + {} + ) + + return isPlainObject(obj) + ? enumerableProperties + : Object.assign(Object.getPrototypeOf(obj), enumerableProperties) +} diff --git a/src/utils/getRequestOptionsByUrl.ts b/src/utils/getRequestOptionsByUrl.ts new file mode 100644 index 0000000..33eeeac --- /dev/null +++ b/src/utils/getRequestOptionsByUrl.ts @@ -0,0 +1,29 @@ +import { RequestOptions } from 'http' + +/** + * Converts a URL instance into the RequestOptions object expected by + * the `ClientRequest` class. + * @see https://github.com/nodejs/node/blob/908292cf1f551c614a733d858528ffb13fb3a524/lib/internal/url.js#L1257 + */ +export function getRequestOptionsByUrl(url: URL): RequestOptions { + const options: RequestOptions = { + method: 'GET', + protocol: url.protocol, + hostname: + typeof url.hostname === 'string' && url.hostname.startsWith('[') + ? url.hostname.slice(1, -1) + : url.hostname, + host: url.host, + path: `${url.pathname}${url.search || ''}`, + } + + if (!!url.port) { + options.port = Number(url.port) + } + + if (url.username || url.password) { + options.auth = `${url.username}:${url.password}` + } + + return options +} diff --git a/src/utils/getUrlByRequestOptions.test.ts b/src/utils/getUrlByRequestOptions.test.ts new file mode 100644 index 0000000..29c715b --- /dev/null +++ b/src/utils/getUrlByRequestOptions.test.ts @@ -0,0 +1,134 @@ +import { Agent as HttpAgent } from 'http' +import { RequestOptions, Agent as HttpsAgent } from 'https' +import { getUrlByRequestOptions } from './getUrlByRequestOptions' + +it('returns a URL based on the basic RequestOptions', () => { + expect( + getUrlByRequestOptions({ + protocol: 'https:', + host: '127.0.0.1', + path: '/resource', + }).href + ).toBe('https://127.0.0.1/resource') +}) + +it('inherits protocol and port from http.Agent, if set', () => { + expect( + getUrlByRequestOptions({ + host: '127.0.0.1', + path: '/', + agent: new HttpAgent(), + }).href + ).toBe('http://127.0.0.1/') +}) + +it('inherits protocol and port from https.Agent, if set', () => { + expect( + getUrlByRequestOptions({ + host: '127.0.0.1', + path: '/', + agent: new HttpsAgent({ + port: 3080, + }), + }).href + ).toBe('https://127.0.0.1:3080/') +}) + +it('resolves protocol to "http" given no explicit protocol and no certificate', () => { + expect( + getUrlByRequestOptions({ + host: '127.0.0.1', + path: '/', + }).href + ).toBe('http://127.0.0.1/') +}) + +it('resolves protocol to "https" given no explicit protocol, but certificate', () => { + expect( + getUrlByRequestOptions({ + host: '127.0.0.1', + path: '/secure', + cert: '', + }).href + ).toBe('https://127.0.0.1/secure') +}) + +it('resolves protocol to "https" given no explicit protocol, but port is 443', () => { + expect( + getUrlByRequestOptions({ + host: '127.0.0.1', + port: 443, + path: '/resource', + }).href + ).toBe('https://127.0.0.1/resource') +}) + +it('resolves protocol to "https" given no explicit protocol, but agent port is 443', () => { + expect( + getUrlByRequestOptions({ + host: '127.0.0.1', + agent: new HttpsAgent({ + port: 443, + }), + path: '/resource', + }).href + ).toBe('https://127.0.0.1/resource') +}) + +it('respects explicitly provided port', () => { + expect( + getUrlByRequestOptions({ + protocol: 'http:', + host: '127.0.0.1', + port: 4002, + path: '/', + }).href + ).toBe('http://127.0.0.1:4002/') +}) + +it('inherits "username" and "password"', () => { + const url = getUrlByRequestOptions({ + protocol: 'https:', + host: '127.0.0.1', + path: '/user', + auth: 'admin:abc-123', + }) + + expect(url).toBeInstanceOf(URL) + expect(url).toHaveProperty('username', 'admin') + expect(url).toHaveProperty('password', 'abc-123') + expect(url).toHaveProperty('href', 'https://admin:abc-123@127.0.0.1/user') +}) + +it('resolves hostname to localhost if none provided', () => { + expect(getUrlByRequestOptions({}).hostname).toBe('localhost') +}) + +it('supports "hostname" instead of "host" and "port"', () => { + const options: RequestOptions = { + protocol: 'https:', + hostname: '127.0.0.1:1234', + path: '/resource', + } + + expect(getUrlByRequestOptions(options).href).toBe( + 'https://127.0.0.1:1234/resource' + ) +}) + +it('handles IPv6 hostnames', () => { + expect( + getUrlByRequestOptions({ + host: '::1', + path: '/resource', + }).href + ).toBe('http://[::1]/resource') + + expect( + getUrlByRequestOptions({ + host: '::1', + port: 3001, + path: '/resource', + }).href + ).toBe('http://[::1]:3001/resource') +}) diff --git a/src/utils/getUrlByRequestOptions.ts b/src/utils/getUrlByRequestOptions.ts new file mode 100644 index 0000000..4cab6e1 --- /dev/null +++ b/src/utils/getUrlByRequestOptions.ts @@ -0,0 +1,176 @@ +import { Agent } from 'http'; +import { RequestOptions, Agent as HttpsAgent } from 'https'; +import { pinoLogger } from '../logger'; + +const logger = pinoLogger.child({ module: 'utils getUrlByRequestOptions' }); + +// Request instance constructed by the "request" library +// has a "self" property that has a "uri" field. This is +// reproducible by performing a "XMLHttpRequest" request in JSDOM. +export interface RequestSelf { + uri?: URL; +} + +export type ResolvedRequestOptions = RequestOptions & RequestSelf; + +export const DEFAULT_PATH = '/'; +const DEFAULT_PROTOCOL = 'http:'; +const DEFAULT_HOST = 'localhost'; +const SSL_PORT = 443; + +function getAgent( + options: ResolvedRequestOptions +): Agent | HttpsAgent | undefined { + return options.agent instanceof Agent ? options.agent : undefined; +} + +function getProtocolByRequestOptions(options: ResolvedRequestOptions): string { + if (options.protocol) { + return options.protocol; + } + + const agent = getAgent(options); + const agentProtocol = (agent as RequestOptions)?.protocol; + + if (agentProtocol) { + return agentProtocol; + } + + const port = getPortByRequestOptions(options); + const isSecureRequest = options.cert || port === SSL_PORT; + + return isSecureRequest ? 'https:' : options.uri?.protocol || DEFAULT_PROTOCOL; +} + +function getPortByRequestOptions( + options: ResolvedRequestOptions +): number | undefined { + // Use the explicitly provided port. + if (options.port) { + return Number(options.port); + } + + // Extract the port from the hostname. + if (options.hostname != null) { + const [, extractedPort] = options.hostname.match(/:(\d+)$/) || []; + + if (extractedPort != null) { + return Number(extractedPort); + } + } + + // Otherwise, try to resolve port from the agent. + const agent = getAgent(options); + + if ((agent as HttpsAgent)?.options.port) { + return Number((agent as HttpsAgent).options.port); + } + + if ((agent as RequestOptions)?.defaultPort) { + return Number((agent as RequestOptions).defaultPort); + } + + // Lastly, return undefined indicating that the port + // must inferred from the protocol. Do not infer it here. + return undefined; +} + +function getHostByRequestOptions(options: ResolvedRequestOptions): string { + const { hostname, host } = options; + + // If the hostname is specified, resolve the host from the "host:port" string. + if (hostname != null) { + return hostname.replace(/:\d+$/, ''); + } + + return host || DEFAULT_HOST; +} + +interface RequestAuth { + username: string; + password: string; +} + +function getAuthByRequestOptions( + options: ResolvedRequestOptions +): RequestAuth | undefined { + if (options.auth) { + const [username, password] = options.auth.split(':'); + return { username, password }; + } +} + +/** + * Returns true if host looks like an IPv6 address without surrounding brackets + * It assumes any host containing `:` is definitely not IPv4 and probably IPv6, + * but note that this could include invalid IPv6 addresses as well. + */ +function isRawIPv6Address(host: string): boolean { + return host.includes(':') && !host.startsWith('[') && !host.endsWith(']'); +} + +function getHostname(host: string, port?: number): string { + const portString = typeof port !== 'undefined' ? `:${port}` : ''; + + /** + * @note As of Node >= 17, hosts (including "localhost") can resolve to IPv6 + * addresses, so construct valid URL by surrounding the IPv6 host with brackets. + */ + if (isRawIPv6Address(host)) { + return `[${host}]${portString}`; + } + + if (typeof port === 'undefined') { + return host; + } + + return `${host}${portString}`; +} + +/** + * Creates a `URL` instance from a given `RequestOptions` object. + */ +export function getUrlByRequestOptions(options: ResolvedRequestOptions): URL { + logger.info('request options', options); + + if (options.uri) { + logger.info( + 'constructing url from explicitly provided "options.uri": %s', + options.uri + ); + return new URL(options.uri.href); + } + + logger.info('figuring out url from request options...'); + + const protocol = getProtocolByRequestOptions(options); + logger.info('protocol', protocol); + + const host = getHostByRequestOptions(options); + logger.info('host', host); + + const port = getPortByRequestOptions(options); + logger.info('port', port); + + const hostname = getHostname(host, port); + logger.info('hostname', hostname); + + const path = options.path || DEFAULT_PATH; + logger.info('path', path); + + const credentials = getAuthByRequestOptions(options); + logger.info('credentials', credentials); + + const authString = credentials + ? `${credentials.username}:${credentials.password}@` + : ''; + logger.info('auth string:', authString); + + const url = new URL(`${protocol}//${hostname}${path}`); + url.username = credentials?.username || ''; + url.password = credentials?.password || ''; + + logger.info('created url:', url); + + return url; +} diff --git a/src/utils/isObject.test.ts b/src/utils/isObject.test.ts new file mode 100644 index 0000000..9417a86 --- /dev/null +++ b/src/utils/isObject.test.ts @@ -0,0 +1,19 @@ +import { isObject } from './isObject'; + +it('resolves given an object', () => { + expect(isObject({})).toBe(true); + expect(isObject({ a: 1 })).toBe(true); +}); + +it('rejects given an object-like instance', () => { + expect(isObject([1])).toBe(false); + expect(isObject(function () {})).toBe(false); +}); + +it('rejects given a non-object instance', () => { + expect(isObject(null)).toBe(false); + expect(isObject(undefined)).toBe(false); + expect(isObject(false)).toBe(false); + expect(isObject(123)).toBe(false); + expect(isObject(Symbol('object Object'))).toBe(false); +}); diff --git a/src/utils/isObject.ts b/src/utils/isObject.ts new file mode 100644 index 0000000..0fd0676 --- /dev/null +++ b/src/utils/isObject.ts @@ -0,0 +1,6 @@ +/** + * Determines if a given value is an instance of object. + */ +export function isObject(value: any): value is T { + return Object.prototype.toString.call(value) === '[object Object]' +} diff --git a/src/utils/responseUtils.ts b/src/utils/responseUtils.ts new file mode 100644 index 0000000..19ef310 --- /dev/null +++ b/src/utils/responseUtils.ts @@ -0,0 +1,5 @@ +/** + * Response status codes for responses that cannot have body. + * @see https://fetch.spec.whatwg.org/#statuses + */ +export const responseStatusCodesWithoutBody = [204, 205, 304] diff --git a/yarn.lock b/yarn.lock index ee8db89..a7583ba 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1196,6 +1196,13 @@ brace-expansion@^1.1.7: balanced-match "^1.0.0" concat-map "0.0.1" +brace-expansion@^2.0.1: + version "2.0.1" + resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae" + integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA== + dependencies: + balanced-match "^1.0.0" + braces@^3.0.2: version "3.0.2" resolved "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" @@ -1409,6 +1416,11 @@ color-name@~1.1.4: resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== +colorette@^2.0.7: + version "2.0.20" + resolved "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz#9eb793e6833067f7235902fcd3b09917a000a95a" + integrity sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w== + combined-stream@^1.0.8: version "1.0.8" resolved "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz" @@ -1977,6 +1989,11 @@ express@^4.17.1: utils-merge "1.0.1" vary "~1.1.2" +fast-copy@^3.0.0: + version "3.0.1" + resolved "https://registry.npmjs.org/fast-copy/-/fast-copy-3.0.1.tgz#9e89ef498b8c04c1cd76b33b8e14271658a732aa" + integrity sha512-Knr7NOtK3HWRYGtHoJrjkaWepqT8thIVGAwt0p0aUs1zqkAzXZV4vo9fFNwyb5fcqK1GKYFYxldQdIDVKhUAfA== + fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: version "3.1.3" resolved "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz" @@ -2237,6 +2254,17 @@ glob@^7.1.3, glob@^7.1.4: once "^1.3.0" path-is-absolute "^1.0.0" +glob@^8.0.0: + version "8.1.0" + resolved "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz#d388f656593ef708ee3e34640fdfb99a9fd1c33e" + integrity sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^5.0.1" + once "^1.3.0" + global-dirs@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/global-dirs/-/global-dirs-3.0.0.tgz" @@ -2363,6 +2391,14 @@ headers-polyfill@^4.0.2: resolved "https://registry.npmjs.org/headers-polyfill/-/headers-polyfill-4.0.2.tgz" integrity sha512-EWGTfnTqAO2L/j5HZgoM/3z82L7necsJ0pO9Tp0X1wil3PDLrkypTBRgVO2ExehEEvUycejZD3FuRaXpZZc3kw== +help-me@^4.0.1: + version "4.2.0" + resolved "https://registry.npmjs.org/help-me/-/help-me-4.2.0.tgz#50712bfd799ff1854ae1d312c36eafcea85b0563" + integrity sha512-TAOnTB8Tz5Dw8penUuzHVrKNKlCIbwwbHnXraNJxPwf8LRtE2HlM84RYuezMFcwOJmoYOCWVDyJ8TQGxn9PgxA== + dependencies: + glob "^8.0.0" + readable-stream "^3.6.0" + hexoid@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/hexoid/-/hexoid-1.0.0.tgz" @@ -3039,6 +3075,11 @@ jju@^1.1.0: resolved "https://registry.npmjs.org/jju/-/jju-1.4.0.tgz" integrity sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA== +joycon@^3.1.1: + version "3.1.1" + resolved "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz#bce8596d6ae808f8b68168f5fc69280996894f03" + integrity sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw== + js-sdsl@^4.1.4: version "4.1.5" resolved "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.1.5.tgz" @@ -3360,11 +3401,23 @@ minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: dependencies: brace-expansion "^1.1.7" +minimatch@^5.0.1: + version "5.1.6" + resolved "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz#1cfcb8cf5522ea69952cd2af95ae09477f122a96" + integrity sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g== + dependencies: + brace-expansion "^2.0.1" + minimist@^1.2.0: version "1.2.7" resolved "https://registry.npmjs.org/minimist/-/minimist-1.2.7.tgz" integrity sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g== +minimist@^1.2.6: + version "1.2.8" + resolved "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" + integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== + morgan@^1.10.0: version "1.10.0" resolved "https://registry.npmjs.org/morgan/-/morgan-1.10.0.tgz" @@ -3753,6 +3806,16 @@ process@^0.11.10: resolved "https://registry.npmjs.org/process/-/process-0.11.10.tgz" integrity sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A== +process-warning@^2.0.0: + version "2.3.1" + resolved "https://registry.npmjs.org/process-warning/-/process-warning-2.3.1.tgz#0caf992272c439f45dd416e1407ee25a3d4c778a" + integrity sha512-JjBvFEn7MwFbzUDa2SRtKJSsyO0LlER4V/FmwLMhBlXNbGgGxdyFCxIdMDLerWUycsVUyaoM9QFLvppFy4IWaQ== + +process@^0.11.10: + version "0.11.10" + resolved "https://registry.npmjs.org/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" + integrity sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A== + prompts@^2.0.1: version "2.4.2" resolved "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz#7b57e73b3a48029ad10ebd44f74b01722a4cb069" @@ -3963,6 +4026,11 @@ safe-stable-stringify@^2.3.1: resolved "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== +secure-json-parse@^2.4.0: + version "2.7.0" + resolved "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.7.0.tgz#5a5f9cd6ae47df23dba3151edd06855d47e09862" + integrity sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw== + semver-compare@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz" @@ -4423,6 +4491,11 @@ url-parse-lax@^3.0.0: dependencies: prepend-http "^2.0.0" +util-deprecate@^1.0.1: + version "1.0.2" + resolved "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== + util@^0.12.3: version "0.12.5" resolved "https://registry.npmjs.org/util/-/util-0.12.5.tgz" From dfdb9c952ae7babde414c255b48d59a9c2cc5afb Mon Sep 17 00:00:00 2001 From: kurpav Date: Mon, 4 Dec 2023 11:39:58 +0200 Subject: [PATCH 02/30] feat: fixed tests and minor refactoring --- package.json | 1 - src/interceptor/NodeClientRequest.ts | 5 + src/interceptor/utils/createRequest.ts | 32 ++-- src/interceptor/utils/createResponse.test.ts | 44 ----- src/interceptor/utils/createResponse.ts | 57 ------ src/utils/cloneObject.test.ts | 93 ---------- src/utils/cloneObject.ts | 36 ---- src/utils/getRequestOptionsByUrl.ts | 29 --- src/utils/getUrlByRequestOptions.test.ts | 134 -------------- src/utils/getUrlByRequestOptions.ts | 176 ------------------- src/utils/isObject.test.ts | 19 -- src/utils/isObject.ts | 6 - src/utils/responseUtils.ts | 5 - yarn.lock | 54 +----- 14 files changed, 23 insertions(+), 668 deletions(-) delete mode 100644 src/interceptor/utils/createResponse.test.ts delete mode 100644 src/interceptor/utils/createResponse.ts delete mode 100644 src/utils/cloneObject.test.ts delete mode 100644 src/utils/cloneObject.ts delete mode 100644 src/utils/getRequestOptionsByUrl.ts delete mode 100644 src/utils/getUrlByRequestOptions.test.ts delete mode 100644 src/utils/getUrlByRequestOptions.ts delete mode 100644 src/utils/isObject.test.ts delete mode 100644 src/utils/isObject.ts delete mode 100644 src/utils/responseUtils.ts diff --git a/package.json b/package.json index 131da06..f39f089 100644 --- a/package.json +++ b/package.json @@ -67,7 +67,6 @@ "jest-extended": "^4.0.2", "json-server": "^0.17.0", "openai": "^4.10.0", - "pino-pretty": "^10.2.3", "postgres": "^3.3.4", "prettier": "^2.8.1", "superagent": "^8.0.9", diff --git a/src/interceptor/NodeClientRequest.ts b/src/interceptor/NodeClientRequest.ts index ce6fb7b..a2b9702 100644 --- a/src/interceptor/NodeClientRequest.ts +++ b/src/interceptor/NodeClientRequest.ts @@ -42,6 +42,7 @@ export class NodeClientRequest extends ClientRequest { // Set request buffer to null by default so that GET/HEAD requests // without a body wouldn't suddenly get one. + // used in createRequest utils function this.requestBuffer = null; this.isInterceptable = isInterceptable({ @@ -121,6 +122,10 @@ export class NodeClientRequest extends ClientRequest { if (this.isInterceptable) { emitResponse(this.requestId as string, args[0], this.emitter); } + + if (!this.ignoredDomains.includes(this.url.hostname)) { + emitResponse(this.requestId as string, args[0], this.emitter); + } } return super.emit(event as string, ...args); diff --git a/src/interceptor/utils/createRequest.ts b/src/interceptor/utils/createRequest.ts index fbc4165..0a10834 100644 --- a/src/interceptor/utils/createRequest.ts +++ b/src/interceptor/utils/createRequest.ts @@ -1,22 +1,22 @@ -import type { NodeClientRequest } from '../NodeClientRequest' +import type { NodeClientRequest } from '../NodeClientRequest'; /** * Creates a Fetch API `Request` instance from the given `http.ClientRequest`. */ export function createRequest(clientRequest: NodeClientRequest): Request { - const headers = new Headers() + const headers = new Headers(); - const outgoingHeaders = clientRequest.getHeaders() + const outgoingHeaders = clientRequest.getHeaders(); for (const headerName in outgoingHeaders) { - const headerValue = outgoingHeaders[headerName] + const headerValue = outgoingHeaders[headerName]; if (typeof headerValue === 'undefined') { - continue + continue; } - const valuesList = Array.prototype.concat([], headerValue) + const valuesList = Array.prototype.concat([], headerValue); for (const value of valuesList) { - headers.append(headerName, value.toString()) + headers.append(headerName, value.toString()); } } @@ -26,24 +26,24 @@ export function createRequest(clientRequest: NodeClientRequest): Request { * @see https://github.com/mswjs/interceptors/issues/438 */ if (clientRequest.url.username || clientRequest.url.password) { - const auth = `${clientRequest.url.username || ''}:${clientRequest.url.password || ''}` - headers.set('Authorization', `Basic ${btoa(auth)}`) + const auth = `${clientRequest.url.username || ''}:${ + clientRequest.url.password || '' + }`; + headers.set('Authorization', `Basic ${btoa(auth)}`); // Remove the credentials from the URL since you cannot // construct a Request instance with such a URL. - clientRequest.url.username = '' - clientRequest.url.password = '' + clientRequest.url.username = ''; + clientRequest.url.password = ''; } - const method = clientRequest.method || 'GET' + const method = clientRequest.method || 'GET'; return new Request(clientRequest.url, { method, headers, credentials: 'same-origin', body: - method === 'HEAD' || method === 'GET' - ? null - : clientRequest.requestBuffer, - }) + method === 'HEAD' || method === 'GET' ? null : clientRequest.requestBuffer + }); } diff --git a/src/interceptor/utils/createResponse.test.ts b/src/interceptor/utils/createResponse.test.ts deleted file mode 100644 index d0ac7e9..0000000 --- a/src/interceptor/utils/createResponse.test.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { Socket } from 'net'; -import * as http from 'http'; -import { createResponse } from './createResponse'; -import { responseStatusCodesWithoutBody } from '../../utils/responseUtils'; - -it('creates a fetch api response from http incoming message', async () => { - const message = new http.IncomingMessage(new Socket()); - message.statusCode = 201; - message.statusMessage = 'Created'; - message.headers['content-type'] = 'application/json'; - - const response = createResponse(message); - - message.emit('data', Buffer.from('{"firstName":')); - message.emit('data', Buffer.from('"John"}')); - message.emit('end'); - - expect(response.status).toBe(201); - expect(response.statusText).toBe('Created'); - expect(response.headers.get('content-type')).toBe('application/json'); - expect(await response.json()).toEqual({ firstName: 'John' }); -}); - -it.each(responseStatusCodesWithoutBody)( - 'ignores message body for %i response status', - (responseStatus) => { - const message = new http.IncomingMessage(new Socket()); - message.statusCode = responseStatus; - - const response = createResponse(message); - - // These chunks will be ignored: this response - // cannot have body. We don't forward this error to - // the consumer because it's us who converts the - // internal stream to a Fetch API Response instance. - // Consumers will rely on the Response API when constructing - // mocked responses. - message.emit('data', Buffer.from('hello')); - message.emit('end'); - - expect(response.status).toBe(responseStatus); - expect(response.body).toBe(null); - } -); diff --git a/src/interceptor/utils/createResponse.ts b/src/interceptor/utils/createResponse.ts deleted file mode 100644 index 31af1f7..0000000 --- a/src/interceptor/utils/createResponse.ts +++ /dev/null @@ -1,57 +0,0 @@ -import type { IncomingHttpHeaders, IncomingMessage } from 'http' -import { responseStatusCodesWithoutBody } from '../../utils/responseUtils' - -/** - * Creates a Fetch API `Response` instance from the given - * `http.IncomingMessage` instance. - */ -export function createResponse(message: IncomingMessage): Response { - const responseBodyOrNull = responseStatusCodesWithoutBody.includes( - message.statusCode || 200 - ) - ? null - : new ReadableStream({ - start(controller) { - message.on('data', (chunk) => controller.enqueue(chunk)) - message.on('end', () => controller.close()) - - /** - * @todo Should also listen to the "error" on the message - * and forward it to the controller. Otherwise the stream - * will pend indefinitely. - */ - }, - }) - - return new Response(responseBodyOrNull, { - status: message.statusCode, - statusText: message.statusMessage, - headers: createHeadersFromIncomingHttpHeaders(message.headers), - }) -} - -function createHeadersFromIncomingHttpHeaders( - httpHeaders: IncomingHttpHeaders -): Headers { - const headers = new Headers() - - for (const headerName in httpHeaders) { - const headerValues = httpHeaders[headerName] - - if (typeof headerValues === 'undefined') { - continue - } - - if (Array.isArray(headerValues)) { - headerValues.forEach((headerValue) => { - headers.append(headerName, headerValue) - }) - - continue - } - - headers.set(headerName, headerValues) - } - - return headers -} diff --git a/src/utils/cloneObject.test.ts b/src/utils/cloneObject.test.ts deleted file mode 100644 index fdc62a2..0000000 --- a/src/utils/cloneObject.test.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { cloneObject } from './cloneObject'; - -it('clones a shallow object', () => { - const original = { a: 1, b: 2, c: [1, 2, 3] }; - const clone = cloneObject(original); - - expect(clone).toEqual(original); - - clone.a = 5; - clone.b = 6; - clone.c = [5, 6, 7]; - - expect(clone).toHaveProperty('a', 5); - expect(clone).toHaveProperty('b', 6); - expect(clone).toHaveProperty('c', [5, 6, 7]); - expect(original).toHaveProperty('a', 1); - expect(original).toHaveProperty('b', 2); - expect(original).toHaveProperty('c', [1, 2, 3]); -}); - -it('clones a nested object', () => { - const original = { a: { b: 1 }, c: { d: { e: 2 } } }; - const clone = cloneObject(original); - - expect(clone).toEqual(original); - - clone.a.b = 10; - clone.c.d.e = 20; - - expect(clone).toHaveProperty(['a', 'b'], 10); - expect(clone).toHaveProperty(['c', 'd', 'e'], 20); - expect(original).toHaveProperty(['a', 'b'], 1); - expect(original).toHaveProperty(['c', 'd', 'e'], 2); -}); - -it('clones a class instance', () => { - class Car { - public manufacturer: string; - constructor() { - this.manufacturer = 'Audi'; - } - getManufacturer() { - return this.manufacturer; - } - } - - const car = new Car(); - const clone = cloneObject(car); - - expect(clone).toHaveProperty('manufacturer', 'Audi'); - expect(clone).toHaveProperty('getManufacturer'); - expect(clone.getManufacturer).toBeInstanceOf(Function); - expect(clone.getManufacturer()).toEqual('Audi'); -}); - -it('ignores nested class instances', () => { - class Car { - name: string; - constructor(name: string) { - this.name = name; - } - getName() { - return this.name; - } - } - const original = { - a: 1, - car: new Car('Audi') - }; - const clone = cloneObject(original); - - expect(clone).toEqual(original); - expect(clone.car).toBeInstanceOf(Car); - expect(clone.car.getName()).toEqual('Audi'); - - clone.car = new Car('BMW'); - - expect(clone.car).toBeInstanceOf(Car); - expect(clone.car.getName()).toEqual('BMW'); - expect(original.car).toBeInstanceOf(Car); - expect(original.car.getName()).toEqual('Audi'); -}); - -it('clones an object with null prototype', () => { - const original = { - key: Object.create(null) - }; - const clone = cloneObject(original); - - expect(clone).toEqual({ - key: {} - }); -}); diff --git a/src/utils/cloneObject.ts b/src/utils/cloneObject.ts deleted file mode 100644 index a70a4c9..0000000 --- a/src/utils/cloneObject.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { pinoLogger } from '../logger' - -const logger = pinoLogger.child({ module: 'cloneObject' }) - -function isPlainObject(obj?: Record): boolean { - logger.info('is plain object?', obj) - - if (obj == null || !obj.constructor?.name) { - logger.info('given object is undefined, not a plain object...') - return false - } - - logger.info('checking the object constructor:', obj.constructor.name) - return obj.constructor.name === 'Object' -} - -export function cloneObject>( - obj: ObjectType -): ObjectType { - logger.info('cloning object:', obj) - - const enumerableProperties = Object.entries(obj).reduce>( - (acc, [key, value]) => { - logger.info('analyzing key-value pair:', key, value) - - // Recursively clone only plain objects, omitting class instances. - acc[key] = isPlainObject(value) ? cloneObject(value) : value - return acc - }, - {} - ) - - return isPlainObject(obj) - ? enumerableProperties - : Object.assign(Object.getPrototypeOf(obj), enumerableProperties) -} diff --git a/src/utils/getRequestOptionsByUrl.ts b/src/utils/getRequestOptionsByUrl.ts deleted file mode 100644 index 33eeeac..0000000 --- a/src/utils/getRequestOptionsByUrl.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { RequestOptions } from 'http' - -/** - * Converts a URL instance into the RequestOptions object expected by - * the `ClientRequest` class. - * @see https://github.com/nodejs/node/blob/908292cf1f551c614a733d858528ffb13fb3a524/lib/internal/url.js#L1257 - */ -export function getRequestOptionsByUrl(url: URL): RequestOptions { - const options: RequestOptions = { - method: 'GET', - protocol: url.protocol, - hostname: - typeof url.hostname === 'string' && url.hostname.startsWith('[') - ? url.hostname.slice(1, -1) - : url.hostname, - host: url.host, - path: `${url.pathname}${url.search || ''}`, - } - - if (!!url.port) { - options.port = Number(url.port) - } - - if (url.username || url.password) { - options.auth = `${url.username}:${url.password}` - } - - return options -} diff --git a/src/utils/getUrlByRequestOptions.test.ts b/src/utils/getUrlByRequestOptions.test.ts deleted file mode 100644 index 29c715b..0000000 --- a/src/utils/getUrlByRequestOptions.test.ts +++ /dev/null @@ -1,134 +0,0 @@ -import { Agent as HttpAgent } from 'http' -import { RequestOptions, Agent as HttpsAgent } from 'https' -import { getUrlByRequestOptions } from './getUrlByRequestOptions' - -it('returns a URL based on the basic RequestOptions', () => { - expect( - getUrlByRequestOptions({ - protocol: 'https:', - host: '127.0.0.1', - path: '/resource', - }).href - ).toBe('https://127.0.0.1/resource') -}) - -it('inherits protocol and port from http.Agent, if set', () => { - expect( - getUrlByRequestOptions({ - host: '127.0.0.1', - path: '/', - agent: new HttpAgent(), - }).href - ).toBe('http://127.0.0.1/') -}) - -it('inherits protocol and port from https.Agent, if set', () => { - expect( - getUrlByRequestOptions({ - host: '127.0.0.1', - path: '/', - agent: new HttpsAgent({ - port: 3080, - }), - }).href - ).toBe('https://127.0.0.1:3080/') -}) - -it('resolves protocol to "http" given no explicit protocol and no certificate', () => { - expect( - getUrlByRequestOptions({ - host: '127.0.0.1', - path: '/', - }).href - ).toBe('http://127.0.0.1/') -}) - -it('resolves protocol to "https" given no explicit protocol, but certificate', () => { - expect( - getUrlByRequestOptions({ - host: '127.0.0.1', - path: '/secure', - cert: '', - }).href - ).toBe('https://127.0.0.1/secure') -}) - -it('resolves protocol to "https" given no explicit protocol, but port is 443', () => { - expect( - getUrlByRequestOptions({ - host: '127.0.0.1', - port: 443, - path: '/resource', - }).href - ).toBe('https://127.0.0.1/resource') -}) - -it('resolves protocol to "https" given no explicit protocol, but agent port is 443', () => { - expect( - getUrlByRequestOptions({ - host: '127.0.0.1', - agent: new HttpsAgent({ - port: 443, - }), - path: '/resource', - }).href - ).toBe('https://127.0.0.1/resource') -}) - -it('respects explicitly provided port', () => { - expect( - getUrlByRequestOptions({ - protocol: 'http:', - host: '127.0.0.1', - port: 4002, - path: '/', - }).href - ).toBe('http://127.0.0.1:4002/') -}) - -it('inherits "username" and "password"', () => { - const url = getUrlByRequestOptions({ - protocol: 'https:', - host: '127.0.0.1', - path: '/user', - auth: 'admin:abc-123', - }) - - expect(url).toBeInstanceOf(URL) - expect(url).toHaveProperty('username', 'admin') - expect(url).toHaveProperty('password', 'abc-123') - expect(url).toHaveProperty('href', 'https://admin:abc-123@127.0.0.1/user') -}) - -it('resolves hostname to localhost if none provided', () => { - expect(getUrlByRequestOptions({}).hostname).toBe('localhost') -}) - -it('supports "hostname" instead of "host" and "port"', () => { - const options: RequestOptions = { - protocol: 'https:', - hostname: '127.0.0.1:1234', - path: '/resource', - } - - expect(getUrlByRequestOptions(options).href).toBe( - 'https://127.0.0.1:1234/resource' - ) -}) - -it('handles IPv6 hostnames', () => { - expect( - getUrlByRequestOptions({ - host: '::1', - path: '/resource', - }).href - ).toBe('http://[::1]/resource') - - expect( - getUrlByRequestOptions({ - host: '::1', - port: 3001, - path: '/resource', - }).href - ).toBe('http://[::1]:3001/resource') -}) diff --git a/src/utils/getUrlByRequestOptions.ts b/src/utils/getUrlByRequestOptions.ts deleted file mode 100644 index 4cab6e1..0000000 --- a/src/utils/getUrlByRequestOptions.ts +++ /dev/null @@ -1,176 +0,0 @@ -import { Agent } from 'http'; -import { RequestOptions, Agent as HttpsAgent } from 'https'; -import { pinoLogger } from '../logger'; - -const logger = pinoLogger.child({ module: 'utils getUrlByRequestOptions' }); - -// Request instance constructed by the "request" library -// has a "self" property that has a "uri" field. This is -// reproducible by performing a "XMLHttpRequest" request in JSDOM. -export interface RequestSelf { - uri?: URL; -} - -export type ResolvedRequestOptions = RequestOptions & RequestSelf; - -export const DEFAULT_PATH = '/'; -const DEFAULT_PROTOCOL = 'http:'; -const DEFAULT_HOST = 'localhost'; -const SSL_PORT = 443; - -function getAgent( - options: ResolvedRequestOptions -): Agent | HttpsAgent | undefined { - return options.agent instanceof Agent ? options.agent : undefined; -} - -function getProtocolByRequestOptions(options: ResolvedRequestOptions): string { - if (options.protocol) { - return options.protocol; - } - - const agent = getAgent(options); - const agentProtocol = (agent as RequestOptions)?.protocol; - - if (agentProtocol) { - return agentProtocol; - } - - const port = getPortByRequestOptions(options); - const isSecureRequest = options.cert || port === SSL_PORT; - - return isSecureRequest ? 'https:' : options.uri?.protocol || DEFAULT_PROTOCOL; -} - -function getPortByRequestOptions( - options: ResolvedRequestOptions -): number | undefined { - // Use the explicitly provided port. - if (options.port) { - return Number(options.port); - } - - // Extract the port from the hostname. - if (options.hostname != null) { - const [, extractedPort] = options.hostname.match(/:(\d+)$/) || []; - - if (extractedPort != null) { - return Number(extractedPort); - } - } - - // Otherwise, try to resolve port from the agent. - const agent = getAgent(options); - - if ((agent as HttpsAgent)?.options.port) { - return Number((agent as HttpsAgent).options.port); - } - - if ((agent as RequestOptions)?.defaultPort) { - return Number((agent as RequestOptions).defaultPort); - } - - // Lastly, return undefined indicating that the port - // must inferred from the protocol. Do not infer it here. - return undefined; -} - -function getHostByRequestOptions(options: ResolvedRequestOptions): string { - const { hostname, host } = options; - - // If the hostname is specified, resolve the host from the "host:port" string. - if (hostname != null) { - return hostname.replace(/:\d+$/, ''); - } - - return host || DEFAULT_HOST; -} - -interface RequestAuth { - username: string; - password: string; -} - -function getAuthByRequestOptions( - options: ResolvedRequestOptions -): RequestAuth | undefined { - if (options.auth) { - const [username, password] = options.auth.split(':'); - return { username, password }; - } -} - -/** - * Returns true if host looks like an IPv6 address without surrounding brackets - * It assumes any host containing `:` is definitely not IPv4 and probably IPv6, - * but note that this could include invalid IPv6 addresses as well. - */ -function isRawIPv6Address(host: string): boolean { - return host.includes(':') && !host.startsWith('[') && !host.endsWith(']'); -} - -function getHostname(host: string, port?: number): string { - const portString = typeof port !== 'undefined' ? `:${port}` : ''; - - /** - * @note As of Node >= 17, hosts (including "localhost") can resolve to IPv6 - * addresses, so construct valid URL by surrounding the IPv6 host with brackets. - */ - if (isRawIPv6Address(host)) { - return `[${host}]${portString}`; - } - - if (typeof port === 'undefined') { - return host; - } - - return `${host}${portString}`; -} - -/** - * Creates a `URL` instance from a given `RequestOptions` object. - */ -export function getUrlByRequestOptions(options: ResolvedRequestOptions): URL { - logger.info('request options', options); - - if (options.uri) { - logger.info( - 'constructing url from explicitly provided "options.uri": %s', - options.uri - ); - return new URL(options.uri.href); - } - - logger.info('figuring out url from request options...'); - - const protocol = getProtocolByRequestOptions(options); - logger.info('protocol', protocol); - - const host = getHostByRequestOptions(options); - logger.info('host', host); - - const port = getPortByRequestOptions(options); - logger.info('port', port); - - const hostname = getHostname(host, port); - logger.info('hostname', hostname); - - const path = options.path || DEFAULT_PATH; - logger.info('path', path); - - const credentials = getAuthByRequestOptions(options); - logger.info('credentials', credentials); - - const authString = credentials - ? `${credentials.username}:${credentials.password}@` - : ''; - logger.info('auth string:', authString); - - const url = new URL(`${protocol}//${hostname}${path}`); - url.username = credentials?.username || ''; - url.password = credentials?.password || ''; - - logger.info('created url:', url); - - return url; -} diff --git a/src/utils/isObject.test.ts b/src/utils/isObject.test.ts deleted file mode 100644 index 9417a86..0000000 --- a/src/utils/isObject.test.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { isObject } from './isObject'; - -it('resolves given an object', () => { - expect(isObject({})).toBe(true); - expect(isObject({ a: 1 })).toBe(true); -}); - -it('rejects given an object-like instance', () => { - expect(isObject([1])).toBe(false); - expect(isObject(function () {})).toBe(false); -}); - -it('rejects given a non-object instance', () => { - expect(isObject(null)).toBe(false); - expect(isObject(undefined)).toBe(false); - expect(isObject(false)).toBe(false); - expect(isObject(123)).toBe(false); - expect(isObject(Symbol('object Object'))).toBe(false); -}); diff --git a/src/utils/isObject.ts b/src/utils/isObject.ts deleted file mode 100644 index 0fd0676..0000000 --- a/src/utils/isObject.ts +++ /dev/null @@ -1,6 +0,0 @@ -/** - * Determines if a given value is an instance of object. - */ -export function isObject(value: any): value is T { - return Object.prototype.toString.call(value) === '[object Object]' -} diff --git a/src/utils/responseUtils.ts b/src/utils/responseUtils.ts deleted file mode 100644 index 19ef310..0000000 --- a/src/utils/responseUtils.ts +++ /dev/null @@ -1,5 +0,0 @@ -/** - * Response status codes for responses that cannot have body. - * @see https://fetch.spec.whatwg.org/#statuses - */ -export const responseStatusCodesWithoutBody = [204, 205, 304] diff --git a/yarn.lock b/yarn.lock index a7583ba..ed50ace 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1196,13 +1196,6 @@ brace-expansion@^1.1.7: balanced-match "^1.0.0" concat-map "0.0.1" -brace-expansion@^2.0.1: - version "2.0.1" - resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae" - integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA== - dependencies: - balanced-match "^1.0.0" - braces@^3.0.2: version "3.0.2" resolved "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" @@ -1416,11 +1409,6 @@ color-name@~1.1.4: resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== -colorette@^2.0.7: - version "2.0.20" - resolved "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz#9eb793e6833067f7235902fcd3b09917a000a95a" - integrity sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w== - combined-stream@^1.0.8: version "1.0.8" resolved "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz" @@ -1989,11 +1977,6 @@ express@^4.17.1: utils-merge "1.0.1" vary "~1.1.2" -fast-copy@^3.0.0: - version "3.0.1" - resolved "https://registry.npmjs.org/fast-copy/-/fast-copy-3.0.1.tgz#9e89ef498b8c04c1cd76b33b8e14271658a732aa" - integrity sha512-Knr7NOtK3HWRYGtHoJrjkaWepqT8thIVGAwt0p0aUs1zqkAzXZV4vo9fFNwyb5fcqK1GKYFYxldQdIDVKhUAfA== - fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: version "3.1.3" resolved "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz" @@ -2254,17 +2237,6 @@ glob@^7.1.3, glob@^7.1.4: once "^1.3.0" path-is-absolute "^1.0.0" -glob@^8.0.0: - version "8.1.0" - resolved "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz#d388f656593ef708ee3e34640fdfb99a9fd1c33e" - integrity sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ== - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^5.0.1" - once "^1.3.0" - global-dirs@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/global-dirs/-/global-dirs-3.0.0.tgz" @@ -2357,7 +2329,7 @@ has-proto@^1.0.1: resolved "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz" integrity sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg== -has-symbols@^1.0.2, has-symbols@^1.0.3: +has-symbols@^1.0.3: version "1.0.3" resolved "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz" integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== @@ -2488,7 +2460,7 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@2.0.4, inherits@^2.0.3: +inherits@2, inherits@2.0.4: version "2.0.4" resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== @@ -3075,11 +3047,6 @@ jju@^1.1.0: resolved "https://registry.npmjs.org/jju/-/jju-1.4.0.tgz" integrity sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA== -joycon@^3.1.1: - version "3.1.1" - resolved "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz#bce8596d6ae808f8b68168f5fc69280996894f03" - integrity sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw== - js-sdsl@^4.1.4: version "4.1.5" resolved "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.1.5.tgz" @@ -3401,23 +3368,11 @@ minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: dependencies: brace-expansion "^1.1.7" -minimatch@^5.0.1: - version "5.1.6" - resolved "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz#1cfcb8cf5522ea69952cd2af95ae09477f122a96" - integrity sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g== - dependencies: - brace-expansion "^2.0.1" - minimist@^1.2.0: version "1.2.7" resolved "https://registry.npmjs.org/minimist/-/minimist-1.2.7.tgz" integrity sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g== -minimist@^1.2.6: - version "1.2.8" - resolved "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" - integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== - morgan@^1.10.0: version "1.10.0" resolved "https://registry.npmjs.org/morgan/-/morgan-1.10.0.tgz" @@ -4026,11 +3981,6 @@ safe-stable-stringify@^2.3.1: resolved "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== -secure-json-parse@^2.4.0: - version "2.7.0" - resolved "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.7.0.tgz#5a5f9cd6ae47df23dba3151edd06855d47e09862" - integrity sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw== - semver-compare@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz" From df74aee9c96beb52e8845ba63cefc5d0a7bf9c7d Mon Sep 17 00:00:00 2001 From: kurpav Date: Mon, 4 Dec 2023 18:27:11 +0200 Subject: [PATCH 03/30] test: add config for unit tests --- src/interceptor/utils/createRequest.test.ts | 240 ++++++++++---------- 1 file changed, 125 insertions(+), 115 deletions(-) diff --git a/src/interceptor/utils/createRequest.test.ts b/src/interceptor/utils/createRequest.test.ts index 00ca504..244db3b 100644 --- a/src/interceptor/utils/createRequest.test.ts +++ b/src/interceptor/utils/createRequest.test.ts @@ -1,127 +1,137 @@ -import { NodeClientRequest } from '../NodeClientRequest'; -import { createRequest } from './createRequest'; import { EventEmitter } from 'events'; -const emitter = new EventEmitter(); +import { NodeClientRequest } from '../NodeClientRequest'; +import { createRequest } from './createRequest'; -it('creates a fetch Request with a JSON body', async () => { - const clientRequest = new NodeClientRequest( - [ - new URL('https://api.github.com'), +/** + * TODO: find a way to handle error from NodeClientRequest + * Error: connect ECONNREFUSED ::1:80 + at TCPConnectWrap.afterConnect [as oncomplete] (node:net:1555:16) + Emitted 'error' event on NodeClientRequest instance at: + at NodeClientRequest.emit (supergood-js/src/interceptor/NodeClientRequest.ts:77:22) + */ +describe.skip('createRequest', () => { + const emitter = new EventEmitter(); + + it('creates a fetch Request with a JSON body', async () => { + const clientRequest = new NodeClientRequest( + [ + new URL('https://api.github.com'), + { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + } + }, + () => {} + ], { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - } - }, - () => {} - ], - { - emitter - } - ); - clientRequest.write(JSON.stringify({ firstName: 'John' })); - - const request = createRequest(clientRequest); - - expect(request.method).toBe('POST'); - expect(request.url).toBe('https://api.github.com/'); - expect(request.headers.get('Content-Type')).toBe('application/json'); - expect(await request.json()).toEqual({ firstName: 'John' }); -}); - -it('creates a fetch Request with an empty body', async () => { - const clientRequest = new NodeClientRequest( - [ - new URL('https://api.github.com'), + emitter + } + ); + clientRequest.write(JSON.stringify({ firstName: 'John' })); + + const request = createRequest(clientRequest); + + expect(request.method).toBe('POST'); + expect(request.url).toBe('https://api.github.com/'); + expect(request.headers.get('Content-Type')).toBe('application/json'); + expect(await request.json()).toEqual({ firstName: 'John' }); + }); + + it('creates a fetch Request with an empty body', async () => { + const clientRequest = new NodeClientRequest( + [ + new URL('https://api.github.com'), + { + method: 'GET', + headers: { + Accept: 'application/json' + } + }, + () => {} + ], { - method: 'GET', - headers: { - Accept: 'application/json' - } - }, - () => {} - ], - { - emitter - } - ); - - const request = createRequest(clientRequest); - - expect(request.method).toBe('GET'); - expect(request.url).toBe('https://api.github.com/'); - expect(request.headers.get('Accept')).toBe('application/json'); - expect(request.body).toBe(null); -}); - -it('creates a fetch Request with an empty string body', async () => { - const clientRequest = new NodeClientRequest( - [ - new URL('https://api.github.com'), + emitter + } + ); + + const request = createRequest(clientRequest); + + expect(request.method).toBe('GET'); + expect(request.url).toBe('https://api.github.com/'); + expect(request.headers.get('Accept')).toBe('application/json'); + expect(request.body).toBe(null); + }); + + it('creates a fetch Request with an empty string body', async () => { + const clientRequest = new NodeClientRequest( + [ + new URL('https://api.github.com'), + { + method: 'HEAD' + }, + () => {} + ], { - method: 'HEAD' - }, - () => {} - ], - { - emitter - } - ); - clientRequest.write(''); - - const request = createRequest(clientRequest); - - expect(request.method).toBe('HEAD'); - expect(request.url).toBe('https://api.github.com/'); - expect(request.body).toBe(null); -}); + emitter + } + ); + clientRequest.write(''); -it('creates a fetch Request with an empty password', async () => { - const clientRequest = new NodeClientRequest( - [new URL('https://api.github.com'), { auth: 'username:' }, () => {}], - { - emitter - } - ); - clientRequest.write(''); - - const request = createRequest(clientRequest); - - expect(request.headers.get('Authorization')).toBe( - `Basic ${btoa('username:')}` - ); - expect(request.url).toBe('https://api.github.com/'); -}); + const request = createRequest(clientRequest); -it('creates a fetch Request with an empty username', async () => { - const clientRequest = new NodeClientRequest( - [new URL('https://api.github.com'), { auth: ':password' }, () => {}], - { - emitter - } - ); - clientRequest.write(''); - - const request = createRequest(clientRequest); - - expect(request.headers.get('Authorization')).toBe( - `Basic ${btoa(':password')}` - ); - expect(request.url).toBe('https://api.github.com/'); -}); + expect(request.method).toBe('HEAD'); + expect(request.url).toBe('https://api.github.com/'); + expect(request.body).toBe(null); + }); -it('creates a fetch Request with falsy headers', async () => { - const clientRequest = new NodeClientRequest( - [new URL('https://api.github.com'), { headers: { foo: 0, empty: '' } }], - { - emitter - } - ); - clientRequest.write(''); + it('creates a fetch Request with an empty password', async () => { + const clientRequest = new NodeClientRequest( + [new URL('https://api.github.com'), { auth: 'username:' }, () => {}], + { + emitter + } + ); + clientRequest.write(''); + + const request = createRequest(clientRequest); + + expect(request.headers.get('Authorization')).toBe( + `Basic ${btoa('username:')}` + ); + expect(request.url).toBe('https://api.github.com/'); + }); + + it('creates a fetch Request with an empty username', async () => { + const clientRequest = new NodeClientRequest( + [new URL('https://api.github.com'), { auth: ':password' }, () => {}], + { + emitter + } + ); + clientRequest.write(''); + + const request = createRequest(clientRequest); + + expect(request.headers.get('Authorization')).toBe( + `Basic ${btoa(':password')}` + ); + expect(request.url).toBe('https://api.github.com/'); + }); + + it('creates a fetch Request with falsy headers', async () => { + const clientRequest = new NodeClientRequest( + [new URL('https://api.github.com'), { headers: { foo: 0, empty: '' } }], + { + emitter + } + ); + clientRequest.write(''); - const request = createRequest(clientRequest); + const request = createRequest(clientRequest); - expect(request.headers.get('foo')).toBe('0'); - expect(request.headers.get('empty')).toBe(''); + expect(request.headers.get('foo')).toBe('0'); + expect(request.headers.get('empty')).toBe(''); + }); }); From 4f63298e7c2b245f0f8d51c3b7f095f69b3cb8a7 Mon Sep 17 00:00:00 2001 From: kurpav Date: Mon, 4 Dec 2023 20:06:48 +0200 Subject: [PATCH 04/30] refactor: add node 14 support --- src/interceptor/NodeClientRequest.ts | 1 - src/interceptor/utils/createRequest.test.ts | 137 -------------------- src/interceptor/utils/createRequest.ts | 49 ------- yarn.lock | 21 ++- 4 files changed, 19 insertions(+), 189 deletions(-) delete mode 100644 src/interceptor/utils/createRequest.test.ts delete mode 100644 src/interceptor/utils/createRequest.ts diff --git a/src/interceptor/NodeClientRequest.ts b/src/interceptor/NodeClientRequest.ts index a2b9702..b717ead 100644 --- a/src/interceptor/NodeClientRequest.ts +++ b/src/interceptor/NodeClientRequest.ts @@ -42,7 +42,6 @@ export class NodeClientRequest extends ClientRequest { // Set request buffer to null by default so that GET/HEAD requests // without a body wouldn't suddenly get one. - // used in createRequest utils function this.requestBuffer = null; this.isInterceptable = isInterceptable({ diff --git a/src/interceptor/utils/createRequest.test.ts b/src/interceptor/utils/createRequest.test.ts deleted file mode 100644 index 244db3b..0000000 --- a/src/interceptor/utils/createRequest.test.ts +++ /dev/null @@ -1,137 +0,0 @@ -import { EventEmitter } from 'events'; - -import { NodeClientRequest } from '../NodeClientRequest'; -import { createRequest } from './createRequest'; - -/** - * TODO: find a way to handle error from NodeClientRequest - * Error: connect ECONNREFUSED ::1:80 - at TCPConnectWrap.afterConnect [as oncomplete] (node:net:1555:16) - Emitted 'error' event on NodeClientRequest instance at: - at NodeClientRequest.emit (supergood-js/src/interceptor/NodeClientRequest.ts:77:22) - */ -describe.skip('createRequest', () => { - const emitter = new EventEmitter(); - - it('creates a fetch Request with a JSON body', async () => { - const clientRequest = new NodeClientRequest( - [ - new URL('https://api.github.com'), - { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - } - }, - () => {} - ], - { - emitter - } - ); - clientRequest.write(JSON.stringify({ firstName: 'John' })); - - const request = createRequest(clientRequest); - - expect(request.method).toBe('POST'); - expect(request.url).toBe('https://api.github.com/'); - expect(request.headers.get('Content-Type')).toBe('application/json'); - expect(await request.json()).toEqual({ firstName: 'John' }); - }); - - it('creates a fetch Request with an empty body', async () => { - const clientRequest = new NodeClientRequest( - [ - new URL('https://api.github.com'), - { - method: 'GET', - headers: { - Accept: 'application/json' - } - }, - () => {} - ], - { - emitter - } - ); - - const request = createRequest(clientRequest); - - expect(request.method).toBe('GET'); - expect(request.url).toBe('https://api.github.com/'); - expect(request.headers.get('Accept')).toBe('application/json'); - expect(request.body).toBe(null); - }); - - it('creates a fetch Request with an empty string body', async () => { - const clientRequest = new NodeClientRequest( - [ - new URL('https://api.github.com'), - { - method: 'HEAD' - }, - () => {} - ], - { - emitter - } - ); - clientRequest.write(''); - - const request = createRequest(clientRequest); - - expect(request.method).toBe('HEAD'); - expect(request.url).toBe('https://api.github.com/'); - expect(request.body).toBe(null); - }); - - it('creates a fetch Request with an empty password', async () => { - const clientRequest = new NodeClientRequest( - [new URL('https://api.github.com'), { auth: 'username:' }, () => {}], - { - emitter - } - ); - clientRequest.write(''); - - const request = createRequest(clientRequest); - - expect(request.headers.get('Authorization')).toBe( - `Basic ${btoa('username:')}` - ); - expect(request.url).toBe('https://api.github.com/'); - }); - - it('creates a fetch Request with an empty username', async () => { - const clientRequest = new NodeClientRequest( - [new URL('https://api.github.com'), { auth: ':password' }, () => {}], - { - emitter - } - ); - clientRequest.write(''); - - const request = createRequest(clientRequest); - - expect(request.headers.get('Authorization')).toBe( - `Basic ${btoa(':password')}` - ); - expect(request.url).toBe('https://api.github.com/'); - }); - - it('creates a fetch Request with falsy headers', async () => { - const clientRequest = new NodeClientRequest( - [new URL('https://api.github.com'), { headers: { foo: 0, empty: '' } }], - { - emitter - } - ); - clientRequest.write(''); - - const request = createRequest(clientRequest); - - expect(request.headers.get('foo')).toBe('0'); - expect(request.headers.get('empty')).toBe(''); - }); -}); diff --git a/src/interceptor/utils/createRequest.ts b/src/interceptor/utils/createRequest.ts deleted file mode 100644 index 0a10834..0000000 --- a/src/interceptor/utils/createRequest.ts +++ /dev/null @@ -1,49 +0,0 @@ -import type { NodeClientRequest } from '../NodeClientRequest'; - -/** - * Creates a Fetch API `Request` instance from the given `http.ClientRequest`. - */ -export function createRequest(clientRequest: NodeClientRequest): Request { - const headers = new Headers(); - - const outgoingHeaders = clientRequest.getHeaders(); - for (const headerName in outgoingHeaders) { - const headerValue = outgoingHeaders[headerName]; - - if (typeof headerValue === 'undefined') { - continue; - } - - const valuesList = Array.prototype.concat([], headerValue); - for (const value of valuesList) { - headers.append(headerName, value.toString()); - } - } - - /** - * Translate the authentication from the request URL to - * the request "Authorization" header. - * @see https://github.com/mswjs/interceptors/issues/438 - */ - if (clientRequest.url.username || clientRequest.url.password) { - const auth = `${clientRequest.url.username || ''}:${ - clientRequest.url.password || '' - }`; - headers.set('Authorization', `Basic ${btoa(auth)}`); - - // Remove the credentials from the URL since you cannot - // construct a Request instance with such a URL. - clientRequest.url.username = ''; - clientRequest.url.password = ''; - } - - const method = clientRequest.method || 'GET'; - - return new Request(clientRequest.url, { - method, - headers, - credentials: 'same-origin', - body: - method === 'HEAD' || method === 'GET' ? null : clientRequest.requestBuffer - }); -} diff --git a/yarn.lock b/yarn.lock index ed50ace..fbef820 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2157,6 +2157,11 @@ function-bind@^1.1.1, function-bind@^1.1.2: resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz" integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== +function-bind@^1.1.2: + version "1.1.2" + resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" + integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== + gensync@^1.0.0-beta.2: version "1.0.0-beta.2" resolved "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" @@ -2329,7 +2334,7 @@ has-proto@^1.0.1: resolved "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz" integrity sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg== -has-symbols@^1.0.3: +has-symbols@^1.0.2, has-symbols@^1.0.3: version "1.0.3" resolved "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz" integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== @@ -2371,6 +2376,18 @@ help-me@^4.0.1: glob "^8.0.0" readable-stream "^3.6.0" +hasown@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz#f4c513d454a57b7c7e1650778de226b11700546c" + integrity sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA== + dependencies: + function-bind "^1.1.2" + +headers-polyfill@^4.0.2: + version "4.0.2" + resolved "https://registry.npmjs.org/headers-polyfill/-/headers-polyfill-4.0.2.tgz#9115a76eee3ce8fbf95b6e3c6bf82d936785b44a" + integrity sha512-EWGTfnTqAO2L/j5HZgoM/3z82L7necsJ0pO9Tp0X1wil3PDLrkypTBRgVO2ExehEEvUycejZD3FuRaXpZZc3kw== + hexoid@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/hexoid/-/hexoid-1.0.0.tgz" @@ -2460,7 +2477,7 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@2.0.4: +inherits@2, inherits@2.0.4, inherits@^2.0.3: version "2.0.4" resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== From cb07594359e458d1576751f7659e2ab563919363 Mon Sep 17 00:00:00 2001 From: kurpav Date: Mon, 4 Dec 2023 20:21:41 +0200 Subject: [PATCH 05/30] chore: remove index file for interceptor --- src/interceptor/index.ts | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 src/interceptor/index.ts diff --git a/src/interceptor/index.ts b/src/interceptor/index.ts deleted file mode 100644 index e69de29..0000000 From 3105476c6caf5b8839ec99a47e9d2eb73ce624b9 Mon Sep 17 00:00:00 2001 From: Alex Klarfeld Date: Mon, 4 Dec 2023 14:00:11 -0800 Subject: [PATCH 06/30] Ignore internal URLs by default --- src/interceptor/NodeClientRequest.ts | 4 ++- src/interceptor/utils/isIgnoredRequest.ts | 39 +++++++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) create mode 100644 src/interceptor/utils/isIgnoredRequest.ts diff --git a/src/interceptor/NodeClientRequest.ts b/src/interceptor/NodeClientRequest.ts index b717ead..10f6912 100644 --- a/src/interceptor/NodeClientRequest.ts +++ b/src/interceptor/NodeClientRequest.ts @@ -18,6 +18,8 @@ export type NodeClientOptions = { allowLocalUrls: boolean; baseUrl?: string; ignoredDomains?: string[]; + allowLocalUrls: boolean; + baseUrl?: string; }; export type Protocol = 'http' | 'https'; @@ -122,7 +124,7 @@ export class NodeClientRequest extends ClientRequest { emitResponse(this.requestId as string, args[0], this.emitter); } - if (!this.ignoredDomains.includes(this.url.hostname)) { + if (!this.isAnIgnoredRequest) { emitResponse(this.requestId as string, args[0], this.emitter); } } diff --git a/src/interceptor/utils/isIgnoredRequest.ts b/src/interceptor/utils/isIgnoredRequest.ts new file mode 100644 index 0000000..87888a1 --- /dev/null +++ b/src/interceptor/utils/isIgnoredRequest.ts @@ -0,0 +1,39 @@ +const commonLocalUrlTlds = [ + 'local' +] + +export function isAnIgnoredRequest ({ url, ignoredDomains, baseUrl, allowLocalUrls }: { + url: URL, ignoredDomains: string[], baseUrl: string, allowLocalUrls: boolean +}): boolean { + + const { origin: baseOrigin } = new URL(baseUrl); + const hostname = url.hostname; + + // Ignore intercepting responses to supergood + if(baseOrigin === url.origin) { + return true; + } + + if(!hostname && !allowLocalUrls) { + return true; + } + + const [, tld] = hostname.split('.') + + // Ignore responses without a .com/.net/.org/etc + if(!tld && !allowLocalUrls) { + return true; + } + + // Ignore responses with common TLD's + if(commonLocalUrlTlds.includes(tld) && !allowLocalUrls) { + return true; + } + + // Ignore responses that have been explicitly excluded + if(ignoredDomains.includes(url.hostname)) { + return true + } + + return false; +} From 2f29d8b2daee4a9ddaa2bfdd195fb3d8908648c2 Mon Sep 17 00:00:00 2001 From: Alex Klarfeld Date: Mon, 4 Dec 2023 14:33:38 -0800 Subject: [PATCH 07/30] Hoisting ignored domains --- yarn.lock | 119 ++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 75 insertions(+), 44 deletions(-) diff --git a/yarn.lock b/yarn.lock index fbef820..2585bbd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -579,7 +579,7 @@ "@nodelib/fs.stat" "2.0.5" run-parallel "^1.1.9" -"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2": +"@nodelib/fs.stat@^2.0.2", "@nodelib/fs.stat@2.0.5": version "2.0.5" resolved "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz" integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== @@ -847,7 +847,7 @@ dependencies: "@types/yargs-parser" "*" -"@typescript-eslint/eslint-plugin@^5.49.0": +"@typescript-eslint/eslint-plugin@^5.0.0", "@typescript-eslint/eslint-plugin@^5.49.0": version "5.49.0" resolved "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.49.0.tgz" integrity sha512-IhxabIpcf++TBaBa1h7jtOWyon80SXPRLDq0dVz5SLFC/eW6tofkw/O7Ar3lkx5z5U6wzbKDrl2larprp5kk5Q== @@ -862,7 +862,7 @@ semver "^7.3.7" tsutils "^3.21.0" -"@typescript-eslint/parser@^5.49.0": +"@typescript-eslint/parser@^5.0.0", "@typescript-eslint/parser@^5.49.0": version "5.49.0" resolved "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.49.0.tgz" integrity sha512-veDlZN9mUhGqU31Qiv2qEp+XrJj5fgZpJ8PW30sHU+j/8/e5ruAhLaVDAeznS7A7i4ucb/s8IozpDtt9NqCkZg== @@ -908,7 +908,7 @@ semver "^7.3.7" tsutils "^3.21.0" -"@typescript-eslint/utils@5.49.0", "@typescript-eslint/utils@^5.10.0": +"@typescript-eslint/utils@^5.10.0", "@typescript-eslint/utils@5.49.0": version "5.49.0" resolved "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.49.0.tgz" integrity sha512-cPJue/4Si25FViIb74sHCLtM4nTSBXtLx1d3/QT6mirQ/c65bV8arBEebBJJizfq8W2YyMoPI/WWPFWitmNqnQ== @@ -955,7 +955,7 @@ acorn-jsx@^5.3.2: resolved "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz" integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== -acorn@^8.8.0: +"acorn@^6.0.0 || ^7.0.0 || ^8.0.0", acorn@^8.8.0: version "8.8.2" resolved "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz" integrity sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw== @@ -1156,7 +1156,7 @@ basic-auth@~2.0.1: dependencies: safe-buffer "5.1.2" -body-parser@1.20.1, body-parser@^1.19.0: +body-parser@^1.19.0, body-parser@1.20.1: version "1.20.1" resolved "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz" integrity sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw== @@ -1409,6 +1409,11 @@ color-name@~1.1.4: resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== +color-name@1.1.3: + version "1.1.3" + resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz" + integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw== + combined-stream@^1.0.8: version "1.0.8" resolved "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz" @@ -1602,7 +1607,7 @@ delayed-stream@~1.0.0: resolved "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz" integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== -depd@2.0.0, depd@~2.0.0: +depd@~2.0.0, depd@2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz" integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== @@ -1798,7 +1803,7 @@ eslint-visitor-keys@^3.3.0: resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz" integrity sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA== -eslint@^8.32.0: +eslint@*, "eslint@^6.0.0 || ^7.0.0 || ^8.0.0", "eslint@^7.0.0 || ^8.0.0", eslint@^8.32.0, eslint@>=5, eslint@>=7.0.0, eslint@>=7.28.0: version "8.33.0" resolved "https://registry.npmjs.org/eslint/-/eslint-8.33.0.tgz" integrity sha512-WjOpFQgKK8VrCnAtl8We0SUOy/oVZ5NHykyMiagV1M9r8IFpIJX7DduK6n1mpfhlG7T1NLWm2SuD8QB7KFySaA== @@ -1998,7 +2003,7 @@ fast-glob@^3.2.9: merge2 "^1.3.0" micromatch "^4.0.4" -fast-json-stable-stringify@2.x, fast-json-stable-stringify@^2.0.0, fast-json-stable-stringify@^2.1.0: +fast-json-stable-stringify@^2.0.0, fast-json-stable-stringify@^2.1.0, fast-json-stable-stringify@2.x: version "2.1.0" resolved "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz" integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== @@ -2159,7 +2164,7 @@ function-bind@^1.1.1, function-bind@^1.1.2: function-bind@^1.1.2: version "1.1.2" - resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" + resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz" integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== gensync@^1.0.0-beta.2: @@ -2378,14 +2383,14 @@ help-me@^4.0.1: hasown@^2.0.0: version "2.0.0" - resolved "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz#f4c513d454a57b7c7e1650778de226b11700546c" + resolved "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz" integrity sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA== dependencies: function-bind "^1.1.2" headers-polyfill@^4.0.2: version "4.0.2" - resolved "https://registry.npmjs.org/headers-polyfill/-/headers-polyfill-4.0.2.tgz#9115a76eee3ce8fbf95b6e3c6bf82d936785b44a" + resolved "https://registry.npmjs.org/headers-polyfill/-/headers-polyfill-4.0.2.tgz" integrity sha512-EWGTfnTqAO2L/j5HZgoM/3z82L7necsJ0pO9Tp0X1wil3PDLrkypTBRgVO2ExehEEvUycejZD3FuRaXpZZc3kw== hexoid@^1.0.0: @@ -2477,21 +2482,21 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@2.0.4, inherits@^2.0.3: +inherits@^2.0.3, inherits@2, inherits@2.0.4: version "2.0.4" resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== -ini@2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/ini/-/ini-2.0.0.tgz" - integrity sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA== - ini@~1.3.0: version "1.3.8" resolved "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz" integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== +ini@2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/ini/-/ini-2.0.0.tgz" + integrity sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA== + ipaddr.js@1.9.1: version "1.9.1" resolved "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz" @@ -3228,7 +3233,7 @@ lodash.set@^4.3.2: resolved "https://registry.npmjs.org/lodash.set/-/lodash.set-4.3.2.tgz" integrity sha512-4hNPN5jlm/N/HLMCO43v8BXKq9Z7QdAGc/VGrRD61w8gN9g/6jF9A4L1pbUgBLCffi0w9VsXfTOij5x8iTyFvg== -lodash@4, lodash@^4.17.21: +lodash@^4.17.21, lodash@4: version "4.17.21" resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== @@ -3346,7 +3351,7 @@ micromatch@^4.0.4: braces "^3.0.2" picomatch "^2.3.1" -mime-db@1.52.0, "mime-db@>= 1.43.0 < 2": +"mime-db@>= 1.43.0 < 2", mime-db@1.52.0: version "1.52.0" resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz" integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== @@ -3401,6 +3406,11 @@ morgan@^1.10.0: on-finished "~2.3.0" on-headers "~1.0.2" +ms@^2.0.0: + version "2.1.3" + resolved "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + ms@2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz" @@ -3411,7 +3421,7 @@ ms@2.1.2: resolved "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== -ms@2.1.3, ms@^2.0.0: +ms@2.1.3: version "2.1.3" resolved "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== @@ -3497,13 +3507,6 @@ on-exit-leak-free@^2.1.0: resolved "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz" integrity sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA== -on-finished@2.4.1: - version "2.4.1" - resolved "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz" - integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg== - dependencies: - ee-first "1.1.1" - on-finished@~2.3.0: version "2.3.0" resolved "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz" @@ -3511,6 +3514,13 @@ on-finished@~2.3.0: dependencies: ee-first "1.1.1" +on-finished@2.4.1: + version "2.4.1" + resolved "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz" + integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg== + dependencies: + ee-first "1.1.1" + on-headers@~1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz" @@ -3646,11 +3656,6 @@ path-parse@^1.0.7: resolved "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== -path-to-regexp@0.1.7: - version "0.1.7" - resolved "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz" - integrity sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ== - path-to-regexp@^1.0.3: version "1.8.0" resolved "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz" @@ -3658,6 +3663,11 @@ path-to-regexp@^1.0.3: dependencies: isarray "0.0.1" +path-to-regexp@0.1.7: + version "0.1.7" + resolved "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz" + integrity sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ== + path-type@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz" @@ -3754,7 +3764,7 @@ prettier-linter-helpers@^1.0.0: dependencies: fast-diff "^1.1.2" -prettier@^2.8.1: +prettier@^2.8.1, prettier@>=2.0.0: version "2.8.3" resolved "https://registry.npmjs.org/prettier/-/prettier-2.8.3.tgz" integrity sha512-tJ/oJ4amDihPoufT5sM0Z1SKEuKay8LfVAMlbbhnnkvt6BUserZylqo2PN+p9KeljLr0OHa2rXHU1T8reeoTrw== @@ -3780,12 +3790,12 @@ process@^0.11.10: process-warning@^2.0.0: version "2.3.1" - resolved "https://registry.npmjs.org/process-warning/-/process-warning-2.3.1.tgz#0caf992272c439f45dd416e1407ee25a3d4c778a" + resolved "https://registry.npmjs.org/process-warning/-/process-warning-2.3.1.tgz" integrity sha512-JjBvFEn7MwFbzUDa2SRtKJSsyO0LlER4V/FmwLMhBlXNbGgGxdyFCxIdMDLerWUycsVUyaoM9QFLvppFy4IWaQ== process@^0.11.10: version "0.11.10" - resolved "https://registry.npmjs.org/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" + resolved "https://registry.npmjs.org/process/-/process-0.11.10.tgz" integrity sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A== prompts@^2.0.1: @@ -3866,7 +3876,7 @@ raw-body@2.5.1: iconv-lite "0.4.24" unpipe "1.0.0" -rc@1.2.8, rc@^1.2.8: +rc@^1.2.8, rc@1.2.8: version "1.2.8" resolved "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz" integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw== @@ -3978,16 +3988,16 @@ run-parallel@^1.1.9: dependencies: queue-microtask "^1.2.2" +safe-buffer@~5.2.0, safe-buffer@5.2.1: + version "5.2.1" + resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + safe-buffer@5.1.2: version "5.1.2" resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz" integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== -safe-buffer@5.2.1, safe-buffer@~5.2.0: - version "5.2.1" - resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz" - integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== - safe-stable-stringify@^2.3.1: version "2.4.3" resolved "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.4.3.tgz" @@ -4173,6 +4183,13 @@ streamsearch@^1.1.0: resolved "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz" integrity sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg== +string_decoder@^1.3.0: + version "1.3.0" + resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz" + integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== + dependencies: + safe-buffer "~5.2.0" + string-length@^4.0.1: version "4.0.2" resolved "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz#a8a8dc7bd5c1a82b9b3c8b87e125f66871b6e57a" @@ -4240,6 +4257,20 @@ superagent@^8.0.9: qs "^6.11.0" semver "^7.3.8" +"supergood@file:": + version "1.1.49-beta.1" + resolved "file:" + dependencies: + headers-polyfill "^4.0.2" + lodash.get "^4.4.2" + lodash.set "^4.3.2" + node-cache "^5.1.2" + pino "^8.16.2" + signal-exit "^3.0.7" + supergood "file:" + ts-essentials "^9.4.1" + web-encoding "^1.1.5" + supports-color@^5.3.0: version "5.5.0" resolved "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" @@ -4387,7 +4418,7 @@ typedarray-to-buffer@^3.1.5: dependencies: is-typedarray "^1.0.0" -typescript@^4.9.4: +typescript@^4.9.4, "typescript@>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta", typescript@>=4.1.0, typescript@>=4.3: version "4.9.4" resolved "https://registry.npmjs.org/typescript/-/typescript-4.9.4.tgz" integrity sha512-Uz+dTXYzxXXbsFpM86Wh3dKCxrQqUcVMxwU54orwlJjOpO3ao8L7j5lH+dWfTwgCwIuM9GQ2kvVotzYJMXTBZg== @@ -4411,7 +4442,7 @@ unique-string@^2.0.0: dependencies: crypto-random-string "^2.0.0" -unpipe@1.0.0, unpipe@~1.0.0: +unpipe@~1.0.0, unpipe@1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz" integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== From a2722ee003106d0260eb72cb163e38d815d695b8 Mon Sep 17 00:00:00 2001 From: kurpav Date: Tue, 5 Dec 2023 21:53:21 +0200 Subject: [PATCH 08/30] test: add unit tests for interception logic --- src/interceptor/NodeClientRequest.ts | 3 +- src/interceptor/utils/isIgnoredRequest.ts | 39 ----------------------- 2 files changed, 2 insertions(+), 40 deletions(-) delete mode 100644 src/interceptor/utils/isIgnoredRequest.ts diff --git a/src/interceptor/NodeClientRequest.ts b/src/interceptor/NodeClientRequest.ts index 10f6912..6869632 100644 --- a/src/interceptor/NodeClientRequest.ts +++ b/src/interceptor/NodeClientRequest.ts @@ -20,6 +20,7 @@ export type NodeClientOptions = { ignoredDomains?: string[]; allowLocalUrls: boolean; baseUrl?: string; + ignoredDomains?: string[]; }; export type Protocol = 'http' | 'https'; @@ -124,7 +125,7 @@ export class NodeClientRequest extends ClientRequest { emitResponse(this.requestId as string, args[0], this.emitter); } - if (!this.isAnIgnoredRequest) { + if (this.isInterceptable) { emitResponse(this.requestId as string, args[0], this.emitter); } } diff --git a/src/interceptor/utils/isIgnoredRequest.ts b/src/interceptor/utils/isIgnoredRequest.ts deleted file mode 100644 index 87888a1..0000000 --- a/src/interceptor/utils/isIgnoredRequest.ts +++ /dev/null @@ -1,39 +0,0 @@ -const commonLocalUrlTlds = [ - 'local' -] - -export function isAnIgnoredRequest ({ url, ignoredDomains, baseUrl, allowLocalUrls }: { - url: URL, ignoredDomains: string[], baseUrl: string, allowLocalUrls: boolean -}): boolean { - - const { origin: baseOrigin } = new URL(baseUrl); - const hostname = url.hostname; - - // Ignore intercepting responses to supergood - if(baseOrigin === url.origin) { - return true; - } - - if(!hostname && !allowLocalUrls) { - return true; - } - - const [, tld] = hostname.split('.') - - // Ignore responses without a .com/.net/.org/etc - if(!tld && !allowLocalUrls) { - return true; - } - - // Ignore responses with common TLD's - if(commonLocalUrlTlds.includes(tld) && !allowLocalUrls) { - return true; - } - - // Ignore responses that have been explicitly excluded - if(ignoredDomains.includes(url.hostname)) { - return true - } - - return false; -} From ae74b07dd4bda073c4ccd8d6643702bff178b8d2 Mon Sep 17 00:00:00 2001 From: Alex Klarfeld Date: Sun, 3 Dec 2023 16:47:45 -0800 Subject: [PATCH 09/30] Add remote config fetch --- src/api.ts | 7 +++++- src/utils.ts | 66 +++++++++++++++++++++++++++++++++++++++++++++------- 2 files changed, 64 insertions(+), 9 deletions(-) diff --git a/src/api.ts b/src/api.ts index 91d7910..536d2de 100644 --- a/src/api.ts +++ b/src/api.ts @@ -1,5 +1,5 @@ import { HeaderOptionType, EventRequestType, ErrorPayloadType } from './types'; -import { post } from './utils'; +import { post, get } from './utils'; const postError = async ( errorSinkUrl: string, @@ -33,4 +33,9 @@ const postEvents = async ( return response; }; +const fetchConfig = async (configUrl: string, options: HeaderOptionType) => { + const response = await get(configUrl, options.headers.Authorization); + return response; +} + export { postError, postEvents }; diff --git a/src/utils.ts b/src/utils.ts index d5f3c7d..d5d988d 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -4,7 +4,6 @@ import { RequestType, ResponseType, EventRequestType, - ConfigType, ErrorPayloadType } from './types'; import crypto from 'node:crypto'; @@ -14,8 +13,8 @@ import https from 'https'; import http from 'http'; import { errors } from './constants'; -import set from 'lodash.set'; -import get from 'lodash.get'; +import _set from 'lodash.set'; +import _get from 'lodash.get'; const logger = ({ errorSinkUrl, @@ -92,9 +91,9 @@ const hashValuesFromKeys = ( let objCopy = { ...obj }; for (let i = 0; i < keysToHash.length; i++) { const keyString = keysToHash[i]; - const value = get(objCopy, keyString); + const value = _get(objCopy, keyString); if (value) { - objCopy = set(objCopy, keyString, hashValue(value)); + objCopy = _set(objCopy, keyString, hashValue(value)); } } return objCopy; @@ -148,11 +147,11 @@ const prepareData = ( return events.filter((e) => hashValuesFromKeys(e, keysToHash)); }; -function post( +const post = ( url: string, data: Array | ErrorPayloadType, authorization: string -): Promise { +): Promise => { const dataString = JSON.stringify(data); const packageVersion = version; @@ -203,6 +202,56 @@ function post( }); } +const get = ( + url: string, + authorization: string +): Promise => { + const packageVersion = version; + + const options = { + method: 'GET', + headers: { + Authorization: authorization, + 'supergood-api': 'supergood-js', + 'supergood-api-version': packageVersion + }, + timeout: 5000 // in ms + }; + + return new Promise((resolve, reject) => { + const transport = url.startsWith('https') ? https : http; + const req = transport.request(url, options, (res) => { + if (res && res.statusCode) { + if (res.statusCode === 401) { + return reject(new Error(errors.UNAUTHORIZED)); + } + + if (res.statusCode < 200 || res.statusCode > 299) { + return reject(new Error(`HTTP status code ${res.statusCode}`)); + } + } + + const body = [] as Buffer[]; + res.on('data', (chunk) => body.push(chunk)); + res.on('end', () => { + const resString = Buffer.concat(body).toString(); + resolve(resString); + }); + }); + + req.on('error', (err) => { + reject(err); + }); + + req.on('timeout', () => { + req.destroy(); + reject(new Error('Request time out')); + }); + + req.end(); // Notice there is no req.write() for GET requests + }); +} + const sleep = (ms: number) => { return new Promise((resolve) => setTimeout(resolve, ms)); }; @@ -215,5 +264,6 @@ export { safeParseJson, prepareData, sleep, - post + post, + get }; From ab433c04cd826f806eb17242d6b96fefd5b6783b Mon Sep 17 00:00:00 2001 From: Alex Klarfeld Date: Mon, 4 Dec 2023 10:22:32 -0800 Subject: [PATCH 10/30] Modify type to include /config --- src/types.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/types.ts b/src/types.ts index 7e10e45..d567321 100644 --- a/src/types.ts +++ b/src/types.ts @@ -2,6 +2,7 @@ interface HeaderOptionType { headers: { 'Content-Type': string; Authorization: string; + 'content-encoding'?: string; }; } From 908928fde77787f5e6d83e43fbc31e8ee234a274 Mon Sep 17 00:00:00 2001 From: Alex Klarfeld Date: Mon, 4 Dec 2023 14:52:33 -0800 Subject: [PATCH 11/30] Fixed constants --- src/constants.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/constants.ts b/src/constants.ts index f99da7d..5e16dd4 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -2,6 +2,7 @@ const defaultConfig = { flushInterval: 1000, eventSinkEndpoint: '/events', errorSinkEndpoint: '/errors', + configFetchEndpoint: '/config', allowLocalUrls: false, keysToHash: [], ignoredDomains: [], From 90eca9b4ee99393ee3cd107231e725b47770d1d0 Mon Sep 17 00:00:00 2001 From: Alex Klarfeld Date: Wed, 6 Dec 2023 14:47:13 -0800 Subject: [PATCH 12/30] Add remote config fetching --- src/api.ts | 2 +- src/constants.ts | 1 + src/index.ts | 14 +++++++++++++- src/types.ts | 2 ++ src/utils.ts | 34 ++++++++++++++++------------------ 5 files changed, 33 insertions(+), 20 deletions(-) diff --git a/src/api.ts b/src/api.ts index 536d2de..ae19588 100644 --- a/src/api.ts +++ b/src/api.ts @@ -38,4 +38,4 @@ const fetchConfig = async (configUrl: string, options: HeaderOptionType) => { return response; } -export { postError, postEvents }; +export { postError, postEvents, fetchConfig }; diff --git a/src/constants.ts b/src/constants.ts index 5e16dd4..202003c 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1,5 +1,6 @@ const defaultConfig = { flushInterval: 1000, + configFetchInterval: 5000, eventSinkEndpoint: '/events', errorSinkEndpoint: '/errors', configFetchEndpoint: '/config', diff --git a/src/index.ts b/src/index.ts index f86cdb8..d0cfb6d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,7 +7,7 @@ import { prepareData, sleep } from './utils'; -import { postEvents } from './api'; +import { postEvents, fetchConfig } from './api'; import { HeaderOptionType, @@ -34,6 +34,7 @@ import { FetchInterceptor } from './interceptor/FetchInterceptor'; const Supergood = () => { let eventSinkUrl: string; let errorSinkUrl: string; + let configFetchUrl: string; let headerOptions: HeaderOptionType; let supergoodConfig: ConfigType; @@ -102,6 +103,7 @@ const Supergood = () => { errorSinkUrl = `${baseUrl}${supergoodConfig.errorSinkEndpoint}`; eventSinkUrl = `${baseUrl}${supergoodConfig.eventSinkEndpoint}`; + configFetchUrl = `${baseUrl}${supergoodConfig.configFetchEndpoint}`; headerOptions = getHeaderOptions(clientId, clientSecret); log = logger({ errorSinkUrl, headerOptions }); @@ -196,6 +198,16 @@ const Supergood = () => { // Flushes the cache every milliseconds interval = setInterval(flushCache, supergoodConfig.flushInterval); interval.unref(); + + // Fetch config from server + setInterval(async () => { + try { + const config = await fetchConfig(configFetchUrl, headerOptions); + console.log(JSON.stringify(config, null, 2)); + } catch(e) { + console.log(e); + } + }, supergoodConfig.configFetchInterval); }; const cacheRequest = async (request: RequestType, baseUrl: string) => { diff --git a/src/types.ts b/src/types.ts index d567321..ba32318 100644 --- a/src/types.ts +++ b/src/types.ts @@ -30,12 +30,14 @@ interface ResponseType { interface ConfigType { flushInterval: number; + configFetchInterval: number; ignoredDomains: string[]; allowLocalUrls: boolean; cacheTtl: number; keysToHash: string[]; eventSinkEndpoint: string; // Defaults to {baseUrl}/events if not provided errorSinkEndpoint: string; // Defaults to {baseUrl}/errors if not provided + configFetchEndpoint: string; // Defaults to {baseUrl}/config if not provided waitAfterClose: number; } diff --git a/src/utils.ts b/src/utils.ts index d5d988d..76615b8 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -4,7 +4,8 @@ import { RequestType, ResponseType, EventRequestType, - ErrorPayloadType + ErrorPayloadType, + ConfigType } from './types'; import crypto from 'node:crypto'; import { postError } from './api'; @@ -84,7 +85,8 @@ const getHeaderOptions = ( }; }; -const hashValuesFromKeys = ( +// format: redacted:: +const redactedValuesFromKeys = ( obj: { request?: RequestType; response?: ResponseType }, keysToHash: Array ) => { @@ -93,7 +95,7 @@ const hashValuesFromKeys = ( const keyString = keysToHash[i]; const value = _get(objCopy, keyString); if (value) { - objCopy = _set(objCopy, keyString, hashValue(value)); + objCopy = _set(objCopy, keyString, redactedValue(value)); } } return objCopy; @@ -107,21 +109,13 @@ const safeParseJson = (json: string) => { } }; -const hashValue = ( +const redactedValue = ( input: string | Record | [Record] | undefined ) => { - const hash = crypto.createHash('sha1'); if (!input) return ''; - - if (Array.isArray(input)) { - return [hash.update(JSON.stringify(input)).digest('base64')]; - } - if (typeof input === 'object') { - return { hashed: hash.update(JSON.stringify(input)).digest('base64') }; - } - if (typeof input === 'string') { - return hash.update(input).digest('base64'); - } + let dataLength = new Blob([input as any]).size; + const dataType = typeof input; + return `redacted:${dataLength}:${dataType}`; }; const getPayloadSize = ( @@ -144,7 +138,7 @@ const prepareData = ( events: Array, keysToHash: Array ) => { - return events.filter((e) => hashValuesFromKeys(e, keysToHash)); + return events.filter((e) => redactedValuesFromKeys(e, keysToHash)); }; const post = ( @@ -252,14 +246,18 @@ const get = ( }); } +const processRemoteConfig = (oldConfig: ConfigType, newConfig: ConfigType) => { + const { ignoredDomains, keysToHash } = oldConfig; +} + const sleep = (ms: number) => { return new Promise((resolve) => setTimeout(resolve, ms)); }; export { getHeaderOptions, - hashValue, - hashValuesFromKeys, + redactedValue, + redactedValuesFromKeys, logger, safeParseJson, prepareData, From 424f8839f4c44346924f06a70027167f22be99c4 Mon Sep 17 00:00:00 2001 From: Alex Klarfeld Date: Thu, 7 Dec 2023 10:40:04 -0800 Subject: [PATCH 13/30] Placeholder for remote config fetching --- .vscode/launch.json | 22 ++++++ .vscode/settings.json | 9 +-- src/api.ts | 7 +- src/constants.ts | 4 +- src/index.ts | 75 ++++++++++--------- src/types.ts | 23 ++++-- src/utils.ts | 160 +++++++++++++++++++++++++++++++---------- test/utils/mock-api.ts | 5 +- 8 files changed, 220 insertions(+), 85 deletions(-) create mode 100644 .vscode/launch.json diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..06aeff5 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,22 @@ +{ + "configurations": [ + { + "command": "yarn worker:data", + "name": "Run Data Worker", + "request": "launch", + "type": "node-terminal" + }, + { + "command": "yarn worker:cron", + "name": "Run Cron Worker", + "request": "launch", + "type": "node-terminal" + }, + { + "command": "yarn server", + "name": "Run Server", + "request": "launch", + "type": "node-terminal" + }, + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json index 1311aaf..413e1c6 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -3,8 +3,9 @@ "source.fixAll.eslint": true }, "eslint.validate": [ - "javascript", - "typescript", + "javascript" ], - "editor.tabSize": 2 -} + "editor.tabSize": 2, + "typescript.tsdk": "node_modules/typescript/lib", + "typescript.enablePromptUseWorkspaceTsdk": true +} \ No newline at end of file diff --git a/src/api.ts b/src/api.ts index ae19588..5a4eb0f 100644 --- a/src/api.ts +++ b/src/api.ts @@ -29,13 +29,12 @@ const postEvents = async ( data, options.headers.Authorization ); - return response; }; -const fetchConfig = async (configUrl: string, options: HeaderOptionType) => { +const fetchRemoteConfig = async (configUrl: string, options: HeaderOptionType) => { const response = await get(configUrl, options.headers.Authorization); - return response; + return JSON.parse(response); } -export { postError, postEvents, fetchConfig }; +export { postError, postEvents, fetchRemoteConfig }; diff --git a/src/constants.ts b/src/constants.ts index 202003c..609e63d 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1,11 +1,11 @@ const defaultConfig = { flushInterval: 1000, - configFetchInterval: 5000, + remoteConfigFetchInterval: 10000, eventSinkEndpoint: '/events', errorSinkEndpoint: '/errors', configFetchEndpoint: '/config', allowLocalUrls: false, - keysToHash: [], + keysToRedact: [], ignoredDomains: [], // After the close command is sent, wait for this many milliseconds before diff --git a/src/index.ts b/src/index.ts index d0cfb6d..a720d4d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,17 +5,18 @@ import { logger, safeParseJson, prepareData, - sleep + sleep, + processRemoteConfig, + getEndpointConfigForRequest } from './utils'; -import { postEvents, fetchConfig } from './api'; +import { postEvents, fetchRemoteConfig } from './api'; import { HeaderOptionType, EventRequestType, ConfigType, LoggerType, - RequestType, - MetadataType + RequestType } from './types'; import { defaultConfig, @@ -34,17 +35,17 @@ import { FetchInterceptor } from './interceptor/FetchInterceptor'; const Supergood = () => { let eventSinkUrl: string; let errorSinkUrl: string; - let configFetchUrl: string; + let remoteConfigFetchUrl: string; let headerOptions: HeaderOptionType; let supergoodConfig: ConfigType; - let supergoodMetadata: MetadataType; let requestCache: NodeCache; let responseCache: NodeCache; let log: LoggerType; - let interval: NodeJS.Timeout; + let flushInterval: NodeJS.Timeout; + let remoteConfigFetchInterval: NodeJS.Timeout; let localOnly = false; @@ -60,7 +61,6 @@ const Supergood = () => { clientId?: string; clientSecret?: string; config?: Partial; - metadata?: Partial; } = { clientId: process.env.SUPERGOOD_CLIENT_ID as string, clientSecret: process.env.SUPERGOOD_CLIENT_SECRET as string, @@ -80,7 +80,6 @@ const Supergood = () => { ...defaultConfig, ...config } as ConfigType; - supergoodMetadata = metadata as MetadataType; requestCache = new NodeCache({ stdTTL: 0 @@ -103,12 +102,33 @@ const Supergood = () => { errorSinkUrl = `${baseUrl}${supergoodConfig.errorSinkEndpoint}`; eventSinkUrl = `${baseUrl}${supergoodConfig.eventSinkEndpoint}`; - configFetchUrl = `${baseUrl}${supergoodConfig.configFetchEndpoint}`; + remoteConfigFetchUrl = `${baseUrl}${supergoodConfig.remoteConfigFetchEndpoint}`; headerOptions = getHeaderOptions(clientId, clientSecret); log = logger({ errorSinkUrl, headerOptions }); - interceptor.setup(); + const fetchAndProcessRemoteConfig = async () => { + try { + const remoteConfigPayload = await fetchRemoteConfig(remoteConfigFetchUrl, headerOptions); + supergoodConfig = { + ...supergoodConfig, + remoteConfig: processRemoteConfig(remoteConfigPayload) + }; + if (supergoodConfig.remoteConfig && !interceptorWasInitialized) { + interceptor.setup(); + interceptorWasInitialized = true; + } + } catch (e) { + log.error(errors.FETCHING_CONFIG, { config: supergoodConfig }, e as Error) + } + }; + + // Fetch and process remote config upon initialization + // Also start up the interception if a remote config is present + await fetchAndProcessRemoteConfig(); + + // Continue fetching the remote config every milliseconds + remoteConfigFetchInterval = setInterval(fetchAndProcessRemoteConfig, supergoodConfig.remoteConfigFetchInterval); interceptor.on( 'request', @@ -196,18 +216,8 @@ const Supergood = () => { ); // Flushes the cache every milliseconds - interval = setInterval(flushCache, supergoodConfig.flushInterval); - interval.unref(); - - // Fetch config from server - setInterval(async () => { - try { - const config = await fetchConfig(configFetchUrl, headerOptions); - console.log(JSON.stringify(config, null, 2)); - } catch(e) { - console.log(e); - } - }, supergoodConfig.configFetchInterval); + flushInterval = setInterval(flushCache, supergoodConfig.flushInterval); + flushInterval.unref(); }; const cacheRequest = async (request: RequestType, baseUrl: string) => { @@ -235,7 +245,7 @@ const Supergood = () => { const responseArray = prepareData( Object.values(responseCache.mget(responseCacheKeys)), - supergoodConfig.keysToHash + supergoodConfig.remoteConfig ) as Array; let data = [...responseArray]; @@ -244,7 +254,7 @@ const Supergood = () => { if (force) { const requestArray = prepareData( Object.values(requestCache.mget(requestCacheKeys)), - supergoodConfig.keysToHash + supergoodConfig.remoteConfig ) as Array; data = [...requestArray, ...responseArray]; } @@ -256,7 +266,7 @@ const Supergood = () => { try { if (localOnly) { - log.debug(JSON.stringify(data, null, 2)); + log.debug(JSON.stringify(data, null, 2), { force }); } else { await postEvents(eventSinkUrl, data, headerOptions); } @@ -266,18 +276,14 @@ const Supergood = () => { if (error.message === errors.UNAUTHORIZED) { log.error( errors.UNAUTHORIZED, - { - config: supergoodConfig, - metadata: { - ...supergoodMetadata - } - }, + { config: supergoodConfig }, error, { reportOut: false } ); - clearInterval(interval); + clearInterval(flushInterval); + clearInterval(remoteConfigFetchInterval); interceptor.teardown(); } else { log.error( @@ -309,7 +315,8 @@ const Supergood = () => { // Stops the interval and disposes of the interceptor const close = async (force = true) => { - clearInterval(interval); + clearInterval(flushInterval); + clearInterval(remoteConfigFetchInterval); // If there are hanging requests, wait a second if (requestCache.keys().length > 0) { diff --git a/src/types.ts b/src/types.ts index ba32318..23b3792 100644 --- a/src/types.ts +++ b/src/types.ts @@ -2,7 +2,6 @@ interface HeaderOptionType { headers: { 'Content-Type': string; Authorization: string; - 'content-encoding'?: string; }; } @@ -30,17 +29,31 @@ interface ResponseType { interface ConfigType { flushInterval: number; - configFetchInterval: number; + remoteConfigFetchInterval: number; ignoredDomains: string[]; allowLocalUrls: boolean; cacheTtl: number; keysToHash: string[]; + remoteConfigFetchEndpoint: string; // Defaults to {baseUrl}/config if not provided eventSinkEndpoint: string; // Defaults to {baseUrl}/events if not provided errorSinkEndpoint: string; // Defaults to {baseUrl}/errors if not provided - configFetchEndpoint: string; // Defaults to {baseUrl}/config if not provided waitAfterClose: number; + remoteConfig: RemoteConfigType; } +interface EndpointConfigType { + location: string; + regex: string; + ignored: boolean; + sensitiveKeys: Array; +} + +interface RemoteConfigType { + [domain: string]: { + [endpointName: string]: EndpointConfigType; + }; +}; + interface MetadataType { numberOfEvents?: number; payloadSize?: number; @@ -98,5 +111,7 @@ export type { ConfigType, ErrorPayloadType, BodyType, - MetadataType + MetadataType, + RemoteConfigType, + EndpointConfigType }; diff --git a/src/utils.ts b/src/utils.ts index 76615b8..bc79900 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -5,7 +5,9 @@ import { ResponseType, EventRequestType, ErrorPayloadType, - ConfigType + ConfigType, + RemoteConfigType, + EndpointConfigType } from './types'; import crypto from 'node:crypto'; import { postError } from './api'; @@ -39,7 +41,6 @@ const logger = ({ JSON.stringify(payload, null, 2), error ); - console.log({ reportOut, errorSinkUrl }); if (reportOut && errorSinkUrl) { postError( errorSinkUrl, @@ -85,20 +86,33 @@ const getHeaderOptions = ( }; }; -// format: redacted:: -const redactedValuesFromKeys = ( +const marshalKeypath = (keypath: string) => { + const [first] = keypath.split('.'); + if(first === 'request_headers') return keypath.replace('request_headers', 'request.headers'); + if(first === 'request_body') return keypath.replace('request_body', 'request.body'); + if(first === 'response_headers') return keypath.replace('response_headers', 'response.headers'); + if(first === 'response_body') return keypath.replace('response_body', 'response.body'); + return keypath; +} + +const redactValuesFromKeys = ( obj: { request?: RequestType; response?: ResponseType }, - keysToHash: Array + remoteConfig: RemoteConfigType ) => { - let objCopy = { ...obj }; - for (let i = 0; i < keysToHash.length; i++) { - const keyString = keysToHash[i]; - const value = _get(objCopy, keyString); - if (value) { - objCopy = _set(objCopy, keyString, redactedValue(value)); + const endpointConfig = getEndpointConfigForRequest(obj.request as RequestType, remoteConfig); + if (!endpointConfig || !endpointConfig?.sensitiveKeys?.length) return obj; + else { + const sensitiveKeys = endpointConfig.sensitiveKeys; + let objCopy = { ...obj }; + for (let i = 0; i < sensitiveKeys.length; i++) { + const keyPath = marshalKeypath(sensitiveKeys[i]); + const value = _get(objCopy, keyPath); + if (value) { + objCopy = _set(objCopy, keyPath, redactValue(value)); + } } + return objCopy; } - return objCopy; }; const safeParseJson = (json: string) => { @@ -109,36 +123,41 @@ const safeParseJson = (json: string) => { } }; -const redactedValue = ( +const redactValue = ( input: string | Record | [Record] | undefined ) => { - if (!input) return ''; - let dataLength = new Blob([input as any]).size; - const dataType = typeof input; - return `redacted:${dataLength}:${dataType}`; -}; + let dataLength; + let dataType; -const getPayloadSize = ( - input: string | Record | [Record] | undefined -) => { - if (!input) return 0; - - if (Array.isArray(input)) { - return JSON.stringify(input).length; + if(!input) { + dataLength = 0; + dataType = 'null'; } - if (typeof input === 'object') { - return JSON.stringify(input).length; + else if (Array.isArray(input)) { + dataLength = input.length; + dataType = 'array'; } - if (typeof input === 'string') { - return input.length; + else if (typeof input === 'object') { + dataLength = new Blob([input.toString()]).size; + dataType = 'object'; + } else if (typeof input === 'string') { + dataLength = input.length; + dataType = 'string'; + } else if (typeof input === 'number') { + dataLength = (input as number).toString().length; + dataType = Number.isInteger(input) ? 'integer' : 'float'; + } else if (typeof input === 'boolean') { + dataLength = 1; + dataType = 'boolean'; } + return `redacted:${dataLength}:${dataType}` }; const prepareData = ( events: Array, - keysToHash: Array + remoteConfig: RemoteConfigType, ) => { - return events.filter((e) => redactedValuesFromKeys(e, keysToHash)); + return events.filter((e) => redactValuesFromKeys(e, remoteConfig)); }; const post = ( @@ -246,22 +265,91 @@ const get = ( }); } -const processRemoteConfig = (oldConfig: ConfigType, newConfig: ConfigType) => { - const { ignoredDomains, keysToHash } = oldConfig; +type RemoteConfigPayload = Array<{ + domain: string; + endpoints: Array<{ + name: string; + matchingRegex: { + regex: string; + location: string; + }; + endpointConfiguration: { + action: string; + sensitiveKeys: Array< + { + keyPath: string; + }>; + } + }>; +}>; + + +const processRemoteConfig = (remoteConfigPayload: RemoteConfigPayload) => { + return (remoteConfigPayload || []).reduce((remoteConfig, domainConfig) => { + const { domain, endpoints } = domainConfig; + const endpointConfig = endpoints.reduce((endpointConfig, endpoint) => { + const { matchingRegex, endpointConfiguration } = endpoint; + const { regex, location } = matchingRegex; + const { action, sensitiveKeys } = endpointConfiguration; + endpointConfig[regex] = { + location, + regex, + ignored: action === 'Ignore', + sensitiveKeys: (sensitiveKeys || []).map((key) => key.keyPath) + }; + return endpointConfig; + }, {} as { [endpointName: string]: EndpointConfigType }); + remoteConfig[domain] = endpointConfig; + return remoteConfig; + }, {} as RemoteConfigType); } const sleep = (ms: number) => { return new Promise((resolve) => setTimeout(resolve, ms)); }; +const getStrRepresentationFromPath = (request: RequestType, location: string) => { + const url = new URL(request.url); + if(location === 'domain') return url.hostname.toString(); + if(location === 'url') return url.toString(); + if(location === 'path') return url.pathname.toString(); + if(location === 'request_headers') return request.headers.toString(); + if(location === 'request_body') return request.body?.toString(); + return request[location as keyof RequestType]?.toString(); +} + +const getEndpointConfigForRequest = (request: RequestType, remoteConfig: RemoteConfigType) => { + const domains = Object.keys(remoteConfig); + const domain = domains.find((domain) => request.url.includes(domain)); + // If the domain doesn't exist in the config, then we return nothing + if (!domain) return null; + const endpointConfigs = remoteConfig[domain]; + for (let i = 0; i < Object.keys(endpointConfigs).length; i++) { + const endpointConfig = endpointConfigs[i]; + const { regex, location } = endpointConfig; + const regexObj = new RegExp(regex); + const strRepresentation = getStrRepresentationFromPath(request, location); + if (!strRepresentation) continue; + else { + const match = regexObj.test(strRepresentation); + if (match) { + return endpointConfig; + } + } + } + return null; +} + export { + processRemoteConfig, getHeaderOptions, - redactedValue, - redactedValuesFromKeys, + redactValue, + redactValuesFromKeys, logger, safeParseJson, prepareData, sleep, post, - get + get, + getEndpointConfigForRequest }; diff --git a/test/utils/mock-api.ts b/test/utils/mock-api.ts index 2254e9d..09a11ce 100644 --- a/test/utils/mock-api.ts +++ b/test/utils/mock-api.ts @@ -7,6 +7,9 @@ export function mockApi() { const postErrorMock = jest .spyOn(api, 'postError') .mockImplementation(async (_, payload) => ({ payload } as any)); + const fetchRemoteConfigMock = jest + .spyOn(api, 'fetchRemoteConfig') + .mockImplementation(async () => ([] as any)); - return { postEventsMock, postErrorMock }; + return { postEventsMock, postErrorMock, fetchRemoteConfigMock }; } From 24719900685737bdcea564c877345db416eb35ce Mon Sep 17 00:00:00 2001 From: Alex Klarfeld Date: Thu, 7 Dec 2023 11:11:57 -0800 Subject: [PATCH 14/30] Added eslint config --- .eslintrc.js | 21 ++-- package.json | 2 +- src/constants.ts | 2 +- src/index.ts | 5 +- test/e2e/core.e2e.test.ts | 2 +- yarn.lock | 241 ++++++++++++++++++++++++-------------- 6 files changed, 171 insertions(+), 102 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 661f901..c996278 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -6,19 +6,14 @@ module.exports = { node: true, 'jest/globals': true }, - extends: [ - 'eslint:recommended', - 'plugin:react/recommended', - 'plugin:@typescript-eslint/eslint-recommended', - 'plugin:@typescript-eslint/recommended', - 'prettier' - ], - parser: '@typescript-eslint/parser', + extends: ['eslint:recommended', 'prettier'], overrides: [], parserOptions: { - ecmaVersion: 'latest' + ecmaVersion: 'latest', + sourceType: 'module', + allowImportExportEverywhere: true }, - plugins: ['react', 'prettier', 'jest', '@typescript-eslint'], + plugins: ['prettier', 'jest'], rules: { // 'indent': ['error', 2], // 'quotes': ['error', 'single'], @@ -28,5 +23,11 @@ module.exports = { // 'no-multi-spaces': ['error'], // 'max-len': ['error', 80], 'prettier/prettier': 2 + // 'react/jsx-max-props-per-line': [ + // 1, + // { + // maximum: 1 + // } + // ] } }; diff --git a/package.json b/package.json index f39f089..f9b6650 100644 --- a/package.json +++ b/package.json @@ -55,7 +55,7 @@ "@types/lodash.set": "^4.3.7", "@types/signal-exit": "^3.0.1", "@types/superagent": "^4.1.16", - "@typescript-eslint/eslint-plugin": "^5.49.0", + "@typescript-eslint/eslint-plugin": "^6.13.2", "@typescript-eslint/parser": "^5.49.0", "axios": "^1.4.0", "dotenv": "^16.0.3", diff --git a/src/constants.ts b/src/constants.ts index 609e63d..cd7f874 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -10,7 +10,7 @@ const defaultConfig = { // After the close command is sent, wait for this many milliseconds before // exiting. This gives any hanging responses a chance to return. - waitAfterClose: 1000 + waitAfterClose: 1000, }; const errors = { diff --git a/src/index.ts b/src/index.ts index a720d4d..7e9fbe8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -117,6 +117,7 @@ const Supergood = () => { if (supergoodConfig.remoteConfig && !interceptorWasInitialized) { interceptor.setup(); interceptorWasInitialized = true; + log.debug('Ready to intercept requests', { config: supergoodConfig }); } } catch (e) { log.error(errors.FETCHING_CONFIG, { config: supergoodConfig }, e as Error) @@ -125,7 +126,9 @@ const Supergood = () => { // Fetch and process remote config upon initialization // Also start up the interception if a remote config is present - await fetchAndProcessRemoteConfig(); + // await fetchAndProcessRemoteConfig(); + const initializeInterceptors = () => { + interceptor.setup(); // Continue fetching the remote config every milliseconds remoteConfigFetchInterval = setInterval(fetchAndProcessRemoteConfig, supergoodConfig.remoteConfigFetchInterval); diff --git a/test/e2e/core.e2e.test.ts b/test/e2e/core.e2e.test.ts index ed09d6d..ab6668e 100644 --- a/test/e2e/core.e2e.test.ts +++ b/test/e2e/core.e2e.test.ts @@ -153,7 +153,7 @@ describe('core functionality', () => { }); }); - describe('config specifications', () => { + xdescribe('config specifications', () => { test('hashing', async () => { await Supergood.init( { diff --git a/yarn.lock b/yarn.lock index 2585bbd..eb2b8b6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -297,6 +297,18 @@ resolved "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== +"@eslint-community/eslint-utils@^4.4.0": + version "4.4.0" + resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz#a23514e8fb9af1269d5f7788aa556798d61c6b59" + integrity sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA== + dependencies: + eslint-visitor-keys "^3.3.0" + +"@eslint-community/regexpp@^4.5.1": + version "4.10.0" + resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.10.0.tgz#548f6de556857c8bb73bbee70c35dc82a2e74d63" + integrity sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA== + "@eslint/eslintrc@^1.4.1": version "1.4.1" resolved "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.4.1.tgz" @@ -579,7 +591,7 @@ "@nodelib/fs.stat" "2.0.5" run-parallel "^1.1.9" -"@nodelib/fs.stat@^2.0.2", "@nodelib/fs.stat@2.0.5": +"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2": version "2.0.5" resolved "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz" integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== @@ -729,6 +741,11 @@ expect "^29.0.0" pretty-format "^29.0.0" +"@types/json-schema@^7.0.12": + version "7.0.15" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" + integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== + "@types/json-schema@^7.0.9": version "7.0.11" resolved "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz" @@ -809,6 +826,11 @@ resolved "https://registry.npmjs.org/@types/semver/-/semver-7.3.12.tgz" integrity sha512-WwA1MW0++RfXmCr12xeYOOC5baSC9mSb0ZqCquFzKhcoF4TvHu5MKOuXsncgZcpVFhB1pXd5hZmM0ryAoCp12A== +"@types/semver@^7.5.0": + version "7.5.6" + resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.6.tgz#c65b2bfce1bec346582c07724e3f8c1017a20339" + integrity sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A== + "@types/serve-static@*": version "1.15.0" resolved "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.0.tgz" @@ -847,22 +869,24 @@ dependencies: "@types/yargs-parser" "*" -"@typescript-eslint/eslint-plugin@^5.0.0", "@typescript-eslint/eslint-plugin@^5.49.0": - version "5.49.0" - resolved "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.49.0.tgz" - integrity sha512-IhxabIpcf++TBaBa1h7jtOWyon80SXPRLDq0dVz5SLFC/eW6tofkw/O7Ar3lkx5z5U6wzbKDrl2larprp5kk5Q== +"@typescript-eslint/eslint-plugin@^6.13.2": + version "6.13.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.13.2.tgz#2e03506c5362a65e43cb132c37c9ce2d3cb51470" + integrity sha512-3+9OGAWHhk4O1LlcwLBONbdXsAhLjyCFogJY/cWy2lxdVJ2JrcTF2pTGMaLl2AE7U1l31n8Py4a8bx5DLf/0dQ== dependencies: - "@typescript-eslint/scope-manager" "5.49.0" - "@typescript-eslint/type-utils" "5.49.0" - "@typescript-eslint/utils" "5.49.0" + "@eslint-community/regexpp" "^4.5.1" + "@typescript-eslint/scope-manager" "6.13.2" + "@typescript-eslint/type-utils" "6.13.2" + "@typescript-eslint/utils" "6.13.2" + "@typescript-eslint/visitor-keys" "6.13.2" debug "^4.3.4" - ignore "^5.2.0" - natural-compare-lite "^1.4.0" - regexpp "^3.2.0" - semver "^7.3.7" - tsutils "^3.21.0" + graphemer "^1.4.0" + ignore "^5.2.4" + natural-compare "^1.4.0" + semver "^7.5.4" + ts-api-utils "^1.0.1" -"@typescript-eslint/parser@^5.0.0", "@typescript-eslint/parser@^5.49.0": +"@typescript-eslint/parser@^5.49.0": version "5.49.0" resolved "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.49.0.tgz" integrity sha512-veDlZN9mUhGqU31Qiv2qEp+XrJj5fgZpJ8PW30sHU+j/8/e5ruAhLaVDAeznS7A7i4ucb/s8IozpDtt9NqCkZg== @@ -880,21 +904,34 @@ "@typescript-eslint/types" "5.49.0" "@typescript-eslint/visitor-keys" "5.49.0" -"@typescript-eslint/type-utils@5.49.0": - version "5.49.0" - resolved "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.49.0.tgz" - integrity sha512-eUgLTYq0tR0FGU5g1YHm4rt5H/+V2IPVkP0cBmbhRyEmyGe4XvJ2YJ6sYTmONfjmdMqyMLad7SB8GvblbeESZA== +"@typescript-eslint/scope-manager@6.13.2": + version "6.13.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-6.13.2.tgz#5fa4e4adace028dafac212c770640b94e7b61052" + integrity sha512-CXQA0xo7z6x13FeDYCgBkjWzNqzBn8RXaE3QVQVIUm74fWJLkJkaHmHdKStrxQllGh6Q4eUGyNpMe0b1hMkXFA== dependencies: - "@typescript-eslint/typescript-estree" "5.49.0" - "@typescript-eslint/utils" "5.49.0" + "@typescript-eslint/types" "6.13.2" + "@typescript-eslint/visitor-keys" "6.13.2" + +"@typescript-eslint/type-utils@6.13.2": + version "6.13.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-6.13.2.tgz#ebec2da14a6bb7122e0fd31eea72a382c39c6102" + integrity sha512-Qr6ssS1GFongzH2qfnWKkAQmMUyZSyOr0W54nZNU1MDfo+U4Mv3XveeLZzadc/yq8iYhQZHYT+eoXJqnACM1tw== + dependencies: + "@typescript-eslint/typescript-estree" "6.13.2" + "@typescript-eslint/utils" "6.13.2" debug "^4.3.4" - tsutils "^3.21.0" + ts-api-utils "^1.0.1" "@typescript-eslint/types@5.49.0": version "5.49.0" resolved "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.49.0.tgz" integrity sha512-7If46kusG+sSnEpu0yOz2xFv5nRz158nzEXnJFCGVEHWnuzolXKwrH5Bsf9zsNlOQkyZuk0BZKKoJQI+1JPBBg== +"@typescript-eslint/types@6.13.2": + version "6.13.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-6.13.2.tgz#c044aac24c2f6cefb8e921e397acad5417dd0ae6" + integrity sha512-7sxbQ+EMRubQc3wTfTsycgYpSujyVbI1xw+3UMRUcrhSy+pN09y/lWzeKDbvhoqcRbHdc+APLs/PWYi/cisLPg== + "@typescript-eslint/typescript-estree@5.49.0": version "5.49.0" resolved "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.49.0.tgz" @@ -908,7 +945,33 @@ semver "^7.3.7" tsutils "^3.21.0" -"@typescript-eslint/utils@^5.10.0", "@typescript-eslint/utils@5.49.0": +"@typescript-eslint/typescript-estree@6.13.2": + version "6.13.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-6.13.2.tgz#ae556ee154c1acf025b48d37c3ef95a1d55da258" + integrity sha512-SuD8YLQv6WHnOEtKv8D6HZUzOub855cfPnPMKvdM/Bh1plv1f7Q/0iFUDLKKlxHcEstQnaUU4QZskgQq74t+3w== + dependencies: + "@typescript-eslint/types" "6.13.2" + "@typescript-eslint/visitor-keys" "6.13.2" + debug "^4.3.4" + globby "^11.1.0" + is-glob "^4.0.3" + semver "^7.5.4" + ts-api-utils "^1.0.1" + +"@typescript-eslint/utils@6.13.2": + version "6.13.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-6.13.2.tgz#8eb89e53adc6d703a879b131e528807245486f89" + integrity sha512-b9Ptq4eAZUym4idijCRzl61oPCwwREcfDI8xGk751Vhzig5fFZR9CyzDz4Sp/nxSLBYxUPyh4QdIDqWykFhNmQ== + dependencies: + "@eslint-community/eslint-utils" "^4.4.0" + "@types/json-schema" "^7.0.12" + "@types/semver" "^7.5.0" + "@typescript-eslint/scope-manager" "6.13.2" + "@typescript-eslint/types" "6.13.2" + "@typescript-eslint/typescript-estree" "6.13.2" + semver "^7.5.4" + +"@typescript-eslint/utils@^5.10.0": version "5.49.0" resolved "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.49.0.tgz" integrity sha512-cPJue/4Si25FViIb74sHCLtM4nTSBXtLx1d3/QT6mirQ/c65bV8arBEebBJJizfq8W2YyMoPI/WWPFWitmNqnQ== @@ -930,6 +993,14 @@ "@typescript-eslint/types" "5.49.0" eslint-visitor-keys "^3.3.0" +"@typescript-eslint/visitor-keys@6.13.2": + version "6.13.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-6.13.2.tgz#e0a4a80cf842bb08e6127b903284166ac4a5594c" + integrity sha512-OGznFs0eAQXJsp+xSd6k/O1UbFi/K/L7WjqeRoFE7vadjAF9y0uppXhYNQNEqygjou782maGClOoZwPqF0Drlw== + dependencies: + "@typescript-eslint/types" "6.13.2" + eslint-visitor-keys "^3.4.1" + "@zxing/text-encoding@0.9.0": version "0.9.0" resolved "https://registry.npmjs.org/@zxing/text-encoding/-/text-encoding-0.9.0.tgz" @@ -955,7 +1026,7 @@ acorn-jsx@^5.3.2: resolved "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz" integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== -"acorn@^6.0.0 || ^7.0.0 || ^8.0.0", acorn@^8.8.0: +acorn@^8.8.0: version "8.8.2" resolved "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz" integrity sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw== @@ -1156,7 +1227,7 @@ basic-auth@~2.0.1: dependencies: safe-buffer "5.1.2" -body-parser@^1.19.0, body-parser@1.20.1: +body-parser@1.20.1, body-parser@^1.19.0: version "1.20.1" resolved "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz" integrity sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw== @@ -1414,6 +1485,11 @@ color-name@1.1.3: resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz" integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw== +color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + combined-stream@^1.0.8: version "1.0.8" resolved "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz" @@ -1607,7 +1683,7 @@ delayed-stream@~1.0.0: resolved "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz" integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== -depd@~2.0.0, depd@2.0.0: +depd@2.0.0, depd@~2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz" integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== @@ -1803,7 +1879,12 @@ eslint-visitor-keys@^3.3.0: resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz" integrity sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA== -eslint@*, "eslint@^6.0.0 || ^7.0.0 || ^8.0.0", "eslint@^7.0.0 || ^8.0.0", eslint@^8.32.0, eslint@>=5, eslint@>=7.0.0, eslint@>=7.28.0: +eslint-visitor-keys@^3.4.1: + version "3.4.3" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800" + integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag== + +eslint@^8.32.0: version "8.33.0" resolved "https://registry.npmjs.org/eslint/-/eslint-8.33.0.tgz" integrity sha512-WjOpFQgKK8VrCnAtl8We0SUOy/oVZ5NHykyMiagV1M9r8IFpIJX7DduK6n1mpfhlG7T1NLWm2SuD8QB7KFySaA== @@ -2003,7 +2084,7 @@ fast-glob@^3.2.9: merge2 "^1.3.0" micromatch "^4.0.4" -fast-json-stable-stringify@^2.0.0, fast-json-stable-stringify@^2.1.0, fast-json-stable-stringify@2.x: +fast-json-stable-stringify@2.x, fast-json-stable-stringify@^2.0.0, fast-json-stable-stringify@^2.1.0: version "2.1.0" resolved "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz" integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== @@ -2317,6 +2398,11 @@ grapheme-splitter@^1.0.4: resolved "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz" integrity sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ== +graphemer@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6" + integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag== + has-flag@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" @@ -2448,6 +2534,11 @@ ignore@^5.2.0: resolved "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz" integrity sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ== +ignore@^5.2.4: + version "5.3.0" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.0.tgz#67418ae40d34d6999c95ff56016759c718c82f78" + integrity sha512-g7dmpshy+gD7mh88OC9NwSGTKoc3kyLAZQRU1mt53Aw/vnvfXnbC+F/7F7QoYVKbV+KNvJx8wArewKy1vXMtlg== + import-fresh@^3.0.0, import-fresh@^3.2.1: version "3.3.0" resolved "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz" @@ -2482,21 +2573,21 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@^2.0.3, inherits@2, inherits@2.0.4: +inherits@2, inherits@2.0.4, inherits@^2.0.3: version "2.0.4" resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== -ini@~1.3.0: - version "1.3.8" - resolved "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz" - integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== - ini@2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/ini/-/ini-2.0.0.tgz" integrity sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA== +ini@~1.3.0: + version "1.3.8" + resolved "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz" + integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== + ipaddr.js@1.9.1: version "1.9.1" resolved "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz" @@ -3233,7 +3324,7 @@ lodash.set@^4.3.2: resolved "https://registry.npmjs.org/lodash.set/-/lodash.set-4.3.2.tgz" integrity sha512-4hNPN5jlm/N/HLMCO43v8BXKq9Z7QdAGc/VGrRD61w8gN9g/6jF9A4L1pbUgBLCffi0w9VsXfTOij5x8iTyFvg== -lodash@^4.17.21, lodash@4: +lodash@4, lodash@^4.17.21: version "4.17.21" resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== @@ -3351,7 +3442,7 @@ micromatch@^4.0.4: braces "^3.0.2" picomatch "^2.3.1" -"mime-db@>= 1.43.0 < 2", mime-db@1.52.0: +mime-db@1.52.0, "mime-db@>= 1.43.0 < 2": version "1.52.0" resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz" integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== @@ -3406,11 +3497,6 @@ morgan@^1.10.0: on-finished "~2.3.0" on-headers "~1.0.2" -ms@^2.0.0: - version "2.1.3" - resolved "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz" - integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== - ms@2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz" @@ -3421,7 +3507,7 @@ ms@2.1.2: resolved "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== -ms@2.1.3: +ms@2.1.3, ms@^2.0.0: version "2.1.3" resolved "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== @@ -3431,11 +3517,6 @@ nanoid@^3.1.23: resolved "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz" integrity sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw== -natural-compare-lite@^1.4.0: - version "1.4.0" - resolved "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz" - integrity sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g== - natural-compare@^1.4.0: version "1.4.0" resolved "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" @@ -3507,13 +3588,6 @@ on-exit-leak-free@^2.1.0: resolved "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz" integrity sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA== -on-finished@~2.3.0: - version "2.3.0" - resolved "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz" - integrity sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww== - dependencies: - ee-first "1.1.1" - on-finished@2.4.1: version "2.4.1" resolved "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz" @@ -3521,6 +3595,13 @@ on-finished@2.4.1: dependencies: ee-first "1.1.1" +on-finished@~2.3.0: + version "2.3.0" + resolved "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz" + integrity sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww== + dependencies: + ee-first "1.1.1" + on-headers@~1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz" @@ -3656,6 +3737,11 @@ path-parse@^1.0.7: resolved "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== +path-to-regexp@0.1.7: + version "0.1.7" + resolved "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz" + integrity sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ== + path-to-regexp@^1.0.3: version "1.8.0" resolved "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz" @@ -3663,11 +3749,6 @@ path-to-regexp@^1.0.3: dependencies: isarray "0.0.1" -path-to-regexp@0.1.7: - version "0.1.7" - resolved "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz" - integrity sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ== - path-type@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz" @@ -3764,7 +3845,7 @@ prettier-linter-helpers@^1.0.0: dependencies: fast-diff "^1.1.2" -prettier@^2.8.1, prettier@>=2.0.0: +prettier@^2.8.1: version "2.8.3" resolved "https://registry.npmjs.org/prettier/-/prettier-2.8.3.tgz" integrity sha512-tJ/oJ4amDihPoufT5sM0Z1SKEuKay8LfVAMlbbhnnkvt6BUserZylqo2PN+p9KeljLr0OHa2rXHU1T8reeoTrw== @@ -3876,7 +3957,7 @@ raw-body@2.5.1: iconv-lite "0.4.24" unpipe "1.0.0" -rc@^1.2.8, rc@1.2.8: +rc@1.2.8, rc@^1.2.8: version "1.2.8" resolved "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz" integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw== @@ -3988,16 +4069,16 @@ run-parallel@^1.1.9: dependencies: queue-microtask "^1.2.2" -safe-buffer@~5.2.0, safe-buffer@5.2.1: - version "5.2.1" - resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz" - integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== - safe-buffer@5.1.2: version "5.1.2" resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz" integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== +safe-buffer@5.2.1, safe-buffer@~5.2.0: + version "5.2.1" + resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + safe-stable-stringify@^2.3.1: version "2.4.3" resolved "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.4.3.tgz" @@ -4183,13 +4264,6 @@ streamsearch@^1.1.0: resolved "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz" integrity sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg== -string_decoder@^1.3.0: - version "1.3.0" - resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz" - integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== - dependencies: - safe-buffer "~5.2.0" - string-length@^4.0.1: version "4.0.2" resolved "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz#a8a8dc7bd5c1a82b9b3c8b87e125f66871b6e57a" @@ -4257,20 +4331,6 @@ superagent@^8.0.9: qs "^6.11.0" semver "^7.3.8" -"supergood@file:": - version "1.1.49-beta.1" - resolved "file:" - dependencies: - headers-polyfill "^4.0.2" - lodash.get "^4.4.2" - lodash.set "^4.3.2" - node-cache "^5.1.2" - pino "^8.16.2" - signal-exit "^3.0.7" - supergood "file:" - ts-essentials "^9.4.1" - web-encoding "^1.1.5" - supports-color@^5.3.0: version "5.5.0" resolved "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" @@ -4350,6 +4410,11 @@ tr46@~0.0.3: resolved "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz" integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== +ts-api-utils@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-1.0.3.tgz#f12c1c781d04427313dbac808f453f050e54a331" + integrity sha512-wNMeqtMz5NtwpT/UZGY5alT+VoKdSsOOP/kqHFcUW1P/VRhH2wJ48+DN2WwUliNbQ976ETwDL0Ifd2VVvgonvg== + ts-essentials@^9.4.1: version "9.4.1" resolved "https://registry.npmjs.org/ts-essentials/-/ts-essentials-9.4.1.tgz" @@ -4418,7 +4483,7 @@ typedarray-to-buffer@^3.1.5: dependencies: is-typedarray "^1.0.0" -typescript@^4.9.4, "typescript@>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta", typescript@>=4.1.0, typescript@>=4.3: +typescript@^4.9.4: version "4.9.4" resolved "https://registry.npmjs.org/typescript/-/typescript-4.9.4.tgz" integrity sha512-Uz+dTXYzxXXbsFpM86Wh3dKCxrQqUcVMxwU54orwlJjOpO3ao8L7j5lH+dWfTwgCwIuM9GQ2kvVotzYJMXTBZg== @@ -4442,7 +4507,7 @@ unique-string@^2.0.0: dependencies: crypto-random-string "^2.0.0" -unpipe@~1.0.0, unpipe@1.0.0: +unpipe@1.0.0, unpipe@~1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz" integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== From 4a19ab979d8f29d7f79d321937f9607bbdc350d5 Mon Sep 17 00:00:00 2001 From: Alex Klarfeld Date: Fri, 8 Dec 2023 15:38:02 -0800 Subject: [PATCH 15/30] Remote config fetch with working sensitive key extraction --- .editorconfig | 675 +++++++++++++++++++++++++++++ .vscode/settings.json | 2 +- src/constants.ts | 1 - src/index.ts | 18 +- src/types.ts | 38 +- src/utils.test.ts | 398 +++++++++++++++++ src/utils.ts | 133 ++++-- test/consts.ts | 4 +- test/e2e/core.e2e.test.ts | 1 - test/e2e/remote-config.e2e.test.ts | 141 ++++++ test/mock-db.js | 15 + test/utils/mock-api.ts | 22 +- 12 files changed, 1386 insertions(+), 62 deletions(-) create mode 100644 .editorconfig create mode 100644 src/utils.test.ts create mode 100644 test/e2e/remote-config.e2e.test.ts diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..12f0cf4 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,675 @@ +[*] +charset = utf-8 +end_of_line = lf +indent_size = 2 +indent_style = space +insert_final_newline = true +max_line_length = 100 +tab_width = 2 +trim_trailing_whitespace = true +ij_continuation_indent_size = 2 +ij_formatter_off_tag = @formatter:off +ij_formatter_on_tag = @formatter:on +ij_formatter_tags_enabled = true +ij_smart_tabs = false +ij_visual_guides = 80 +ij_wrap_on_typing = false + +[*.css] +ij_css_align_closing_brace_with_properties = false +ij_css_blank_lines_around_nested_selector = 1 +ij_css_blank_lines_between_blocks = 1 +ij_css_brace_placement = end_of_line +ij_css_enforce_quotes_on_format = false +ij_css_hex_color_long_format = false +ij_css_hex_color_lower_case = false +ij_css_hex_color_short_format = false +ij_css_hex_color_upper_case = false +ij_css_keep_blank_lines_in_code = 2 +ij_css_keep_indents_on_empty_lines = false +ij_css_keep_single_line_blocks = false +ij_css_properties_order = font,font-family,font-size,font-weight,font-style,font-variant,font-size-adjust,font-stretch,line-height,position,z-index,top,right,bottom,left,display,visibility,float,clear,overflow,overflow-x,overflow-y,clip,zoom,align-content,align-items,align-self,flex,flex-flow,flex-basis,flex-direction,flex-grow,flex-shrink,flex-wrap,justify-content,order,box-sizing,width,min-width,max-width,height,min-height,max-height,margin,margin-top,margin-right,margin-bottom,margin-left,padding,padding-top,padding-right,padding-bottom,padding-left,table-layout,empty-cells,caption-side,border-spacing,border-collapse,list-style,list-style-position,list-style-type,list-style-image,content,quotes,counter-reset,counter-increment,resize,cursor,user-select,nav-index,nav-up,nav-right,nav-down,nav-left,transition,transition-delay,transition-timing-function,transition-duration,transition-property,transform,transform-origin,animation,animation-name,animation-duration,animation-play-state,animation-timing-function,animation-delay,animation-iteration-count,animation-direction,text-align,text-align-last,vertical-align,white-space,text-decoration,text-emphasis,text-emphasis-color,text-emphasis-style,text-emphasis-position,text-indent,text-justify,letter-spacing,word-spacing,text-outline,text-transform,text-wrap,text-overflow,text-overflow-ellipsis,text-overflow-mode,word-wrap,word-break,tab-size,hyphens,pointer-events,opacity,color,border,border-width,border-style,border-color,border-top,border-top-width,border-top-style,border-top-color,border-right,border-right-width,border-right-style,border-right-color,border-bottom,border-bottom-width,border-bottom-style,border-bottom-color,border-left,border-left-width,border-left-style,border-left-color,border-radius,border-top-left-radius,border-top-right-radius,border-bottom-right-radius,border-bottom-left-radius,border-image,border-image-source,border-image-slice,border-image-width,border-image-outset,border-image-repeat,outline,outline-width,outline-style,outline-color,outline-offset,background,background-color,background-image,background-repeat,background-attachment,background-position,background-position-x,background-position-y,background-clip,background-origin,background-size,box-decoration-break,box-shadow,text-shadow +ij_css_space_after_colon = true +ij_css_space_before_opening_brace = true +ij_css_use_double_quotes = true +ij_css_value_alignment = do_not_align + +[*.feature] +indent_size = 2 +ij_gherkin_keep_indents_on_empty_lines = false + +[*.haml] +indent_size = 2 +ij_haml_keep_indents_on_empty_lines = false + +[*.less] +indent_size = 2 +ij_less_align_closing_brace_with_properties = false +ij_less_blank_lines_around_nested_selector = 1 +ij_less_blank_lines_between_blocks = 1 +ij_less_brace_placement = 0 +ij_less_enforce_quotes_on_format = false +ij_less_hex_color_long_format = false +ij_less_hex_color_lower_case = false +ij_less_hex_color_short_format = false +ij_less_hex_color_upper_case = false +ij_less_keep_blank_lines_in_code = 2 +ij_less_keep_indents_on_empty_lines = false +ij_less_keep_single_line_blocks = false +ij_less_properties_order = font,font-family,font-size,font-weight,font-style,font-variant,font-size-adjust,font-stretch,line-height,position,z-index,top,right,bottom,left,display,visibility,float,clear,overflow,overflow-x,overflow-y,clip,zoom,align-content,align-items,align-self,flex,flex-flow,flex-basis,flex-direction,flex-grow,flex-shrink,flex-wrap,justify-content,order,box-sizing,width,min-width,max-width,height,min-height,max-height,margin,margin-top,margin-right,margin-bottom,margin-left,padding,padding-top,padding-right,padding-bottom,padding-left,table-layout,empty-cells,caption-side,border-spacing,border-collapse,list-style,list-style-position,list-style-type,list-style-image,content,quotes,counter-reset,counter-increment,resize,cursor,user-select,nav-index,nav-up,nav-right,nav-down,nav-left,transition,transition-delay,transition-timing-function,transition-duration,transition-property,transform,transform-origin,animation,animation-name,animation-duration,animation-play-state,animation-timing-function,animation-delay,animation-iteration-count,animation-direction,text-align,text-align-last,vertical-align,white-space,text-decoration,text-emphasis,text-emphasis-color,text-emphasis-style,text-emphasis-position,text-indent,text-justify,letter-spacing,word-spacing,text-outline,text-transform,text-wrap,text-overflow,text-overflow-ellipsis,text-overflow-mode,word-wrap,word-break,tab-size,hyphens,pointer-events,opacity,color,border,border-width,border-style,border-color,border-top,border-top-width,border-top-style,border-top-color,border-right,border-right-width,border-right-style,border-right-color,border-bottom,border-bottom-width,border-bottom-style,border-bottom-color,border-left,border-left-width,border-left-style,border-left-color,border-radius,border-top-left-radius,border-top-right-radius,border-bottom-right-radius,border-bottom-left-radius,border-image,border-image-source,border-image-slice,border-image-width,border-image-outset,border-image-repeat,outline,outline-width,outline-style,outline-color,outline-offset,background,background-color,background-image,background-repeat,background-attachment,background-position,background-position-x,background-position-y,background-clip,background-origin,background-size,box-decoration-break,box-shadow,text-shadow +ij_less_space_after_colon = true +ij_less_space_before_opening_brace = true +ij_less_use_double_quotes = true +ij_less_value_alignment = 0 + +[*.sass] +indent_size = 2 +ij_sass_align_closing_brace_with_properties = false +ij_sass_blank_lines_around_nested_selector = 1 +ij_sass_blank_lines_between_blocks = 1 +ij_sass_brace_placement = 0 +ij_sass_enforce_quotes_on_format = false +ij_sass_hex_color_long_format = false +ij_sass_hex_color_lower_case = false +ij_sass_hex_color_short_format = false +ij_sass_hex_color_upper_case = false +ij_sass_keep_blank_lines_in_code = 2 +ij_sass_keep_indents_on_empty_lines = false +ij_sass_keep_single_line_blocks = false +ij_sass_properties_order = font,font-family,font-size,font-weight,font-style,font-variant,font-size-adjust,font-stretch,line-height,position,z-index,top,right,bottom,left,display,visibility,float,clear,overflow,overflow-x,overflow-y,clip,zoom,align-content,align-items,align-self,flex,flex-flow,flex-basis,flex-direction,flex-grow,flex-shrink,flex-wrap,justify-content,order,box-sizing,width,min-width,max-width,height,min-height,max-height,margin,margin-top,margin-right,margin-bottom,margin-left,padding,padding-top,padding-right,padding-bottom,padding-left,table-layout,empty-cells,caption-side,border-spacing,border-collapse,list-style,list-style-position,list-style-type,list-style-image,content,quotes,counter-reset,counter-increment,resize,cursor,user-select,nav-index,nav-up,nav-right,nav-down,nav-left,transition,transition-delay,transition-timing-function,transition-duration,transition-property,transform,transform-origin,animation,animation-name,animation-duration,animation-play-state,animation-timing-function,animation-delay,animation-iteration-count,animation-direction,text-align,text-align-last,vertical-align,white-space,text-decoration,text-emphasis,text-emphasis-color,text-emphasis-style,text-emphasis-position,text-indent,text-justify,letter-spacing,word-spacing,text-outline,text-transform,text-wrap,text-overflow,text-overflow-ellipsis,text-overflow-mode,word-wrap,word-break,tab-size,hyphens,pointer-events,opacity,color,border,border-width,border-style,border-color,border-top,border-top-width,border-top-style,border-top-color,border-right,border-right-width,border-right-style,border-right-color,border-bottom,border-bottom-width,border-bottom-style,border-bottom-color,border-left,border-left-width,border-left-style,border-left-color,border-radius,border-top-left-radius,border-top-right-radius,border-bottom-right-radius,border-bottom-left-radius,border-image,border-image-source,border-image-slice,border-image-width,border-image-outset,border-image-repeat,outline,outline-width,outline-style,outline-color,outline-offset,background,background-color,background-image,background-repeat,background-attachment,background-position,background-position-x,background-position-y,background-clip,background-origin,background-size,box-decoration-break,box-shadow,text-shadow +ij_sass_space_after_colon = true +ij_sass_space_before_opening_brace = true +ij_sass_use_double_quotes = true +ij_sass_value_alignment = 0 + +[*.scss] +indent_size = 2 +ij_scss_align_closing_brace_with_properties = false +ij_scss_blank_lines_around_nested_selector = 1 +ij_scss_blank_lines_between_blocks = 1 +ij_scss_brace_placement = 0 +ij_scss_enforce_quotes_on_format = false +ij_scss_hex_color_long_format = false +ij_scss_hex_color_lower_case = false +ij_scss_hex_color_short_format = false +ij_scss_hex_color_upper_case = false +ij_scss_keep_blank_lines_in_code = 2 +ij_scss_keep_indents_on_empty_lines = false +ij_scss_keep_single_line_blocks = false +ij_scss_properties_order = font,font-family,font-size,font-weight,font-style,font-variant,font-size-adjust,font-stretch,line-height,position,z-index,top,right,bottom,left,display,visibility,float,clear,overflow,overflow-x,overflow-y,clip,zoom,align-content,align-items,align-self,flex,flex-flow,flex-basis,flex-direction,flex-grow,flex-shrink,flex-wrap,justify-content,order,box-sizing,width,min-width,max-width,height,min-height,max-height,margin,margin-top,margin-right,margin-bottom,margin-left,padding,padding-top,padding-right,padding-bottom,padding-left,table-layout,empty-cells,caption-side,border-spacing,border-collapse,list-style,list-style-position,list-style-type,list-style-image,content,quotes,counter-reset,counter-increment,resize,cursor,user-select,nav-index,nav-up,nav-right,nav-down,nav-left,transition,transition-delay,transition-timing-function,transition-duration,transition-property,transform,transform-origin,animation,animation-name,animation-duration,animation-play-state,animation-timing-function,animation-delay,animation-iteration-count,animation-direction,text-align,text-align-last,vertical-align,white-space,text-decoration,text-emphasis,text-emphasis-color,text-emphasis-style,text-emphasis-position,text-indent,text-justify,letter-spacing,word-spacing,text-outline,text-transform,text-wrap,text-overflow,text-overflow-ellipsis,text-overflow-mode,word-wrap,word-break,tab-size,hyphens,pointer-events,opacity,color,border,border-width,border-style,border-color,border-top,border-top-width,border-top-style,border-top-color,border-right,border-right-width,border-right-style,border-right-color,border-bottom,border-bottom-width,border-bottom-style,border-bottom-color,border-left,border-left-width,border-left-style,border-left-color,border-radius,border-top-left-radius,border-top-right-radius,border-bottom-right-radius,border-bottom-left-radius,border-image,border-image-source,border-image-slice,border-image-width,border-image-outset,border-image-repeat,outline,outline-width,outline-style,outline-color,outline-offset,background,background-color,background-image,background-repeat,background-attachment,background-position,background-position-x,background-position-y,background-clip,background-origin,background-size,box-decoration-break,box-shadow,text-shadow +ij_scss_space_after_colon = true +ij_scss_space_before_opening_brace = true +ij_scss_use_double_quotes = true +ij_scss_value_alignment = 0 + +[*.styl] +indent_size = 2 +ij_stylus_align_closing_brace_with_properties = false +ij_stylus_blank_lines_around_nested_selector = 1 +ij_stylus_blank_lines_between_blocks = 1 +ij_stylus_brace_placement = 0 +ij_stylus_enforce_quotes_on_format = false +ij_stylus_hex_color_long_format = false +ij_stylus_hex_color_lower_case = false +ij_stylus_hex_color_short_format = false +ij_stylus_hex_color_upper_case = false +ij_stylus_keep_blank_lines_in_code = 2 +ij_stylus_keep_indents_on_empty_lines = false +ij_stylus_keep_single_line_blocks = false +ij_stylus_properties_order = font,font-family,font-size,font-weight,font-style,font-variant,font-size-adjust,font-stretch,line-height,position,z-index,top,right,bottom,left,display,visibility,float,clear,overflow,overflow-x,overflow-y,clip,zoom,align-content,align-items,align-self,flex,flex-flow,flex-basis,flex-direction,flex-grow,flex-shrink,flex-wrap,justify-content,order,box-sizing,width,min-width,max-width,height,min-height,max-height,margin,margin-top,margin-right,margin-bottom,margin-left,padding,padding-top,padding-right,padding-bottom,padding-left,table-layout,empty-cells,caption-side,border-spacing,border-collapse,list-style,list-style-position,list-style-type,list-style-image,content,quotes,counter-reset,counter-increment,resize,cursor,user-select,nav-index,nav-up,nav-right,nav-down,nav-left,transition,transition-delay,transition-timing-function,transition-duration,transition-property,transform,transform-origin,animation,animation-name,animation-duration,animation-play-state,animation-timing-function,animation-delay,animation-iteration-count,animation-direction,text-align,text-align-last,vertical-align,white-space,text-decoration,text-emphasis,text-emphasis-color,text-emphasis-style,text-emphasis-position,text-indent,text-justify,letter-spacing,word-spacing,text-outline,text-transform,text-wrap,text-overflow,text-overflow-ellipsis,text-overflow-mode,word-wrap,word-break,tab-size,hyphens,pointer-events,opacity,color,border,border-width,border-style,border-color,border-top,border-top-width,border-top-style,border-top-color,border-right,border-right-width,border-right-style,border-right-color,border-bottom,border-bottom-width,border-bottom-style,border-bottom-color,border-left,border-left-width,border-left-style,border-left-color,border-radius,border-top-left-radius,border-top-right-radius,border-bottom-right-radius,border-bottom-left-radius,border-image,border-image-source,border-image-slice,border-image-width,border-image-outset,border-image-repeat,outline,outline-width,outline-style,outline-color,outline-offset,background,background-color,background-image,background-repeat,background-attachment,background-position,background-position-x,background-position-y,background-clip,background-origin,background-size,box-decoration-break,box-shadow,text-shadow +ij_stylus_space_after_colon = true +ij_stylus_space_before_opening_brace = true +ij_stylus_use_double_quotes = true +ij_stylus_value_alignment = 0 + +[*.vue] +indent_size = 2 +tab_width = 2 +ij_continuation_indent_size = 2 +ij_visual_guides = 80 +ij_vue_indent_children_of_top_level = template +ij_vue_interpolation_new_line_after_start_delimiter = false +ij_vue_interpolation_new_line_before_end_delimiter = false +ij_vue_interpolation_wrap = off +ij_vue_keep_indents_on_empty_lines = false +ij_vue_spaces_within_interpolation_expressions = true + +[.editorconfig] +ij_editorconfig_align_group_field_declarations = false +ij_editorconfig_space_after_colon = false +ij_editorconfig_space_after_comma = true +ij_editorconfig_space_before_colon = false +ij_editorconfig_space_before_comma = false +ij_editorconfig_spaces_around_assignment_operators = true + +[{*.ant,*.fxml,*.jhm,*.jnlp,*.jrxml,*.rng,*.tld,*.wsdl,*.xml,*.xsd,*.xsl,*.xslt,*.xul}] +ij_xml_align_attributes = true +ij_xml_align_text = false +ij_xml_attribute_wrap = normal +ij_xml_block_comment_at_first_column = true +ij_xml_keep_blank_lines = 2 +ij_xml_keep_indents_on_empty_lines = false +ij_xml_keep_line_breaks = true +ij_xml_keep_line_breaks_in_text = true +ij_xml_keep_whitespaces = false +ij_xml_keep_whitespaces_around_cdata = preserve +ij_xml_keep_whitespaces_inside_cdata = false +ij_xml_line_comment_at_first_column = true +ij_xml_space_after_tag_name = false +ij_xml_space_around_equals_in_attribute = false +ij_xml_space_inside_empty_tag = false +ij_xml_text_wrap = normal + +[{*.ats,*.ts}] +indent_size = 2 +tab_width = 2 +ij_continuation_indent_size = 2 +ij_visual_guides = 80 +ij_typescript_align_imports = false +ij_typescript_align_multiline_array_initializer_expression = false +ij_typescript_align_multiline_binary_operation = false +ij_typescript_align_multiline_chained_methods = false +ij_typescript_align_multiline_extends_list = false +ij_typescript_align_multiline_for = true +ij_typescript_align_multiline_parameters = true +ij_typescript_align_multiline_parameters_in_calls = false +ij_typescript_align_multiline_ternary_operation = false +ij_typescript_align_object_properties = 0 +ij_typescript_align_union_types = false +ij_typescript_align_var_statements = 0 +ij_typescript_array_initializer_new_line_after_left_brace = false +ij_typescript_array_initializer_right_brace_on_new_line = false +ij_typescript_array_initializer_wrap = off +ij_typescript_assignment_wrap = off +ij_typescript_binary_operation_sign_on_next_line = false +ij_typescript_binary_operation_wrap = off +ij_typescript_blacklist_imports = rxjs/Rx,node_modules/**,**/node_modules/**,@angular/material,@angular/material/typings/** +ij_typescript_blank_lines_after_imports = 1 +ij_typescript_blank_lines_around_class = 1 +ij_typescript_blank_lines_around_field = 0 +ij_typescript_blank_lines_around_field_in_interface = 0 +ij_typescript_blank_lines_around_function = 1 +ij_typescript_blank_lines_around_method = 1 +ij_typescript_blank_lines_around_method_in_interface = 1 +ij_typescript_block_brace_style = end_of_line +ij_typescript_call_parameters_new_line_after_left_paren = false +ij_typescript_call_parameters_right_paren_on_new_line = false +ij_typescript_call_parameters_wrap = off +ij_typescript_catch_on_new_line = false +ij_typescript_chained_call_dot_on_new_line = true +ij_typescript_class_brace_style = end_of_line +ij_typescript_comma_on_new_line = false +ij_typescript_do_while_brace_force = never +ij_typescript_else_on_new_line = false +ij_typescript_enforce_trailing_comma = remove +ij_typescript_extends_keyword_wrap = off +ij_typescript_extends_list_wrap = off +ij_typescript_field_prefix = _ +ij_typescript_file_name_style = relaxed +ij_typescript_finally_on_new_line = false +ij_typescript_for_brace_force = always +ij_typescript_for_statement_new_line_after_left_paren = false +ij_typescript_for_statement_right_paren_on_new_line = false +ij_typescript_for_statement_wrap = off +ij_typescript_force_quote_style = true +ij_typescript_force_semicolon_style = true +ij_typescript_function_expression_brace_style = end_of_line +ij_typescript_if_brace_force = always +ij_typescript_import_merge_members = global +ij_typescript_import_prefer_absolute_path = global +ij_typescript_import_sort_members = true +ij_typescript_import_sort_module_name = false +ij_typescript_import_use_node_resolution = true +ij_typescript_imports_wrap = on_every_item +ij_typescript_indent_case_from_switch = true +ij_typescript_indent_chained_calls = true +ij_typescript_indent_package_children = 0 +ij_typescript_jsdoc_include_types = false +ij_typescript_jsx_attribute_value = braces +ij_typescript_keep_blank_lines_in_code = 1 +ij_typescript_keep_first_column_comment = true +ij_typescript_keep_indents_on_empty_lines = false +ij_typescript_keep_line_breaks = true +ij_typescript_keep_simple_blocks_in_one_line = true +ij_typescript_keep_simple_methods_in_one_line = true +ij_typescript_line_comment_add_space = true +ij_typescript_line_comment_at_first_column = false +ij_typescript_method_brace_style = end_of_line +ij_typescript_method_call_chain_wrap = on_every_item +ij_typescript_method_parameters_new_line_after_left_paren = false +ij_typescript_method_parameters_right_paren_on_new_line = false +ij_typescript_method_parameters_wrap = on_every_item +ij_typescript_object_literal_wrap = on_every_item +ij_typescript_parentheses_expression_new_line_after_left_paren = false +ij_typescript_parentheses_expression_right_paren_on_new_line = false +ij_typescript_place_assignment_sign_on_next_line = false +ij_typescript_prefer_as_type_cast = false +ij_typescript_prefer_explicit_types_function_expression_returns = false +ij_typescript_prefer_explicit_types_function_returns = false +ij_typescript_prefer_explicit_types_vars_fields = false +ij_typescript_prefer_parameters_wrap = false +ij_typescript_reformat_c_style_comments = true +ij_typescript_space_after_colon = true +ij_typescript_space_after_comma = true +ij_typescript_space_after_dots_in_rest_parameter = false +ij_typescript_space_after_generator_mult = true +ij_typescript_space_after_property_colon = true +ij_typescript_space_after_quest = true +ij_typescript_space_after_type_colon = true +ij_typescript_space_after_unary_not = false +ij_typescript_space_before_async_arrow_lparen = true +ij_typescript_space_before_catch_keyword = true +ij_typescript_space_before_catch_left_brace = true +ij_typescript_space_before_catch_parentheses = true +ij_typescript_space_before_class_lbrace = true +ij_typescript_space_before_class_left_brace = true +ij_typescript_space_before_colon = true +ij_typescript_space_before_comma = false +ij_typescript_space_before_do_left_brace = true +ij_typescript_space_before_else_keyword = true +ij_typescript_space_before_else_left_brace = true +ij_typescript_space_before_finally_keyword = true +ij_typescript_space_before_finally_left_brace = true +ij_typescript_space_before_for_left_brace = true +ij_typescript_space_before_for_parentheses = true +ij_typescript_space_before_for_semicolon = false +ij_typescript_space_before_function_left_parenth = true +ij_typescript_space_before_generator_mult = false +ij_typescript_space_before_if_left_brace = true +ij_typescript_space_before_if_parentheses = true +ij_typescript_space_before_method_call_parentheses = false +ij_typescript_space_before_method_left_brace = true +ij_typescript_space_before_method_parentheses = false +ij_typescript_space_before_property_colon = false +ij_typescript_space_before_quest = true +ij_typescript_space_before_switch_left_brace = true +ij_typescript_space_before_switch_parentheses = true +ij_typescript_space_before_try_left_brace = true +ij_typescript_space_before_type_colon = false +ij_typescript_space_before_unary_not = false +ij_typescript_space_before_while_keyword = true +ij_typescript_space_before_while_left_brace = true +ij_typescript_space_before_while_parentheses = true +ij_typescript_spaces_around_additive_operators = true +ij_typescript_spaces_around_arrow_function_operator = true +ij_typescript_spaces_around_assignment_operators = true +ij_typescript_spaces_around_bitwise_operators = true +ij_typescript_spaces_around_equality_operators = true +ij_typescript_spaces_around_logical_operators = true +ij_typescript_spaces_around_multiplicative_operators = true +ij_typescript_spaces_around_relational_operators = true +ij_typescript_spaces_around_shift_operators = true +ij_typescript_spaces_around_unary_operator = false +ij_typescript_spaces_within_array_initializer_brackets = false +ij_typescript_spaces_within_brackets = false +ij_typescript_spaces_within_catch_parentheses = false +ij_typescript_spaces_within_for_parentheses = false +ij_typescript_spaces_within_if_parentheses = false +ij_typescript_spaces_within_imports = true +ij_typescript_spaces_within_interpolation_expressions = false +ij_typescript_spaces_within_method_call_parentheses = false +ij_typescript_spaces_within_method_parentheses = false +ij_typescript_spaces_within_object_literal_braces = true +ij_typescript_spaces_within_object_type_braces = true +ij_typescript_spaces_within_parentheses = false +ij_typescript_spaces_within_switch_parentheses = false +ij_typescript_spaces_within_type_assertion = false +ij_typescript_spaces_within_union_types = true +ij_typescript_spaces_within_while_parentheses = false +ij_typescript_special_else_if_treatment = true +ij_typescript_ternary_operation_signs_on_next_line = true +ij_typescript_ternary_operation_wrap = on_every_item +ij_typescript_union_types_wrap = on_every_item +ij_typescript_use_chained_calls_group_indents = false +ij_typescript_use_double_quotes = false +ij_typescript_use_explicit_js_extension = global +ij_typescript_use_path_mapping = always +ij_typescript_use_public_modifier = false +ij_typescript_use_semicolon_after_statement = true +ij_typescript_var_declaration_wrap = on_every_item +ij_typescript_while_brace_force = always +ij_typescript_while_on_new_line = false +ij_typescript_wrap_comments = false + +[{*.bash,*.sh,*.zsh}] +indent_size = 2 +tab_width = 2 +ij_shell_binary_ops_start_line = false +ij_shell_keep_column_alignment_padding = false +ij_shell_minify_program = false +ij_shell_redirect_followed_by_space = false +ij_shell_switch_cases_indented = false +ij_shell_use_unix_line_separator = true + +[{*.cjs,*.js,*.mjs}] +ij_continuation_indent_size = 2 +ij_visual_guides = none +ij_javascript_align_imports = false +ij_javascript_align_multiline_array_initializer_expression = false +ij_javascript_align_multiline_binary_operation = false +ij_javascript_align_multiline_chained_methods = true +ij_javascript_align_multiline_extends_list = false +ij_javascript_align_multiline_for = false +ij_javascript_align_multiline_parameters = false +ij_javascript_align_multiline_parameters_in_calls = false +ij_javascript_align_multiline_ternary_operation = false +ij_javascript_align_object_properties = 0 +ij_javascript_align_union_types = false +ij_javascript_align_var_statements = 0 +ij_javascript_array_initializer_new_line_after_left_brace = true +ij_javascript_array_initializer_right_brace_on_new_line = true +ij_javascript_array_initializer_wrap = on_every_item +ij_javascript_assignment_wrap = on_every_item +ij_javascript_binary_operation_sign_on_next_line = true +ij_javascript_binary_operation_wrap = on_every_item +ij_javascript_blacklist_imports = rxjs/Rx,node_modules/**,**/node_modules/**,@angular/material,@angular/material/typings/** +ij_javascript_blank_lines_after_imports = 1 +ij_javascript_blank_lines_around_class = 1 +ij_javascript_blank_lines_around_field = 0 +ij_javascript_blank_lines_around_function = 1 +ij_javascript_blank_lines_around_method = 1 +ij_javascript_block_brace_style = end_of_line +ij_javascript_call_parameters_new_line_after_left_paren = false +ij_javascript_call_parameters_right_paren_on_new_line = false +ij_javascript_call_parameters_wrap = on_every_item +ij_javascript_catch_on_new_line = false +ij_javascript_chained_call_dot_on_new_line = true +ij_javascript_class_brace_style = end_of_line +ij_javascript_comma_on_new_line = false +ij_javascript_do_while_brace_force = always +ij_javascript_else_on_new_line = false +ij_javascript_enforce_trailing_comma = remove +ij_javascript_extends_keyword_wrap = off +ij_javascript_extends_list_wrap = off +ij_javascript_field_prefix = _ +ij_javascript_file_name_style = relaxed +ij_javascript_finally_on_new_line = false +ij_javascript_for_brace_force = always +ij_javascript_for_statement_new_line_after_left_paren = false +ij_javascript_for_statement_right_paren_on_new_line = false +ij_javascript_for_statement_wrap = off +ij_javascript_force_quote_style = true +ij_javascript_force_semicolon_style = true +ij_javascript_function_expression_brace_style = end_of_line +ij_javascript_if_brace_force = always +ij_javascript_import_merge_members = global +ij_javascript_import_prefer_absolute_path = global +ij_javascript_import_sort_members = true +ij_javascript_import_sort_module_name = false +ij_javascript_import_use_node_resolution = true +ij_javascript_imports_wrap = on_every_item +ij_javascript_indent_case_from_switch = true +ij_javascript_indent_chained_calls = true +ij_javascript_indent_package_children = 0 +ij_javascript_jsx_attribute_value = braces +ij_javascript_keep_blank_lines_in_code = 1 +ij_javascript_keep_first_column_comment = true +ij_javascript_keep_indents_on_empty_lines = false +ij_javascript_keep_line_breaks = true +ij_javascript_keep_simple_blocks_in_one_line = true +ij_javascript_keep_simple_methods_in_one_line = true +ij_javascript_line_comment_add_space = true +ij_javascript_line_comment_at_first_column = false +ij_javascript_method_brace_style = end_of_line +ij_javascript_method_call_chain_wrap = on_every_item +ij_javascript_method_parameters_new_line_after_left_paren = false +ij_javascript_method_parameters_right_paren_on_new_line = false +ij_javascript_method_parameters_wrap = on_every_item +ij_javascript_object_literal_wrap = on_every_item +ij_javascript_parentheses_expression_new_line_after_left_paren = false +ij_javascript_parentheses_expression_right_paren_on_new_line = false +ij_javascript_place_assignment_sign_on_next_line = false +ij_javascript_prefer_as_type_cast = false +ij_javascript_prefer_explicit_types_function_expression_returns = false +ij_javascript_prefer_explicit_types_function_returns = false +ij_javascript_prefer_explicit_types_vars_fields = false +ij_javascript_prefer_parameters_wrap = true +ij_javascript_reformat_c_style_comments = false +ij_javascript_space_after_colon = true +ij_javascript_space_after_comma = true +ij_javascript_space_after_dots_in_rest_parameter = false +ij_javascript_space_after_generator_mult = true +ij_javascript_space_after_property_colon = true +ij_javascript_space_after_quest = true +ij_javascript_space_after_type_colon = true +ij_javascript_space_after_unary_not = false +ij_javascript_space_before_async_arrow_lparen = true +ij_javascript_space_before_catch_keyword = true +ij_javascript_space_before_catch_left_brace = true +ij_javascript_space_before_catch_parentheses = true +ij_javascript_space_before_class_lbrace = true +ij_javascript_space_before_class_left_brace = true +ij_javascript_space_before_colon = true +ij_javascript_space_before_comma = false +ij_javascript_space_before_do_left_brace = true +ij_javascript_space_before_else_keyword = true +ij_javascript_space_before_else_left_brace = true +ij_javascript_space_before_finally_keyword = true +ij_javascript_space_before_finally_left_brace = true +ij_javascript_space_before_for_left_brace = true +ij_javascript_space_before_for_parentheses = true +ij_javascript_space_before_for_semicolon = false +ij_javascript_space_before_function_left_parenth = true +ij_javascript_space_before_generator_mult = false +ij_javascript_space_before_if_left_brace = true +ij_javascript_space_before_if_parentheses = true +ij_javascript_space_before_method_call_parentheses = false +ij_javascript_space_before_method_left_brace = true +ij_javascript_space_before_method_parentheses = false +ij_javascript_space_before_property_colon = false +ij_javascript_space_before_quest = true +ij_javascript_space_before_switch_left_brace = true +ij_javascript_space_before_switch_parentheses = true +ij_javascript_space_before_try_left_brace = true +ij_javascript_space_before_type_colon = false +ij_javascript_space_before_unary_not = false +ij_javascript_space_before_while_keyword = true +ij_javascript_space_before_while_left_brace = true +ij_javascript_space_before_while_parentheses = true +ij_javascript_spaces_around_additive_operators = true +ij_javascript_spaces_around_arrow_function_operator = true +ij_javascript_spaces_around_assignment_operators = true +ij_javascript_spaces_around_bitwise_operators = true +ij_javascript_spaces_around_equality_operators = true +ij_javascript_spaces_around_logical_operators = true +ij_javascript_spaces_around_multiplicative_operators = true +ij_javascript_spaces_around_relational_operators = true +ij_javascript_spaces_around_shift_operators = true +ij_javascript_spaces_around_unary_operator = false +ij_javascript_spaces_within_array_initializer_brackets = false +ij_javascript_spaces_within_brackets = false +ij_javascript_spaces_within_catch_parentheses = false +ij_javascript_spaces_within_for_parentheses = false +ij_javascript_spaces_within_if_parentheses = false +ij_javascript_spaces_within_imports = true +ij_javascript_spaces_within_interpolation_expressions = false +ij_javascript_spaces_within_method_call_parentheses = false +ij_javascript_spaces_within_method_parentheses = false +ij_javascript_spaces_within_object_literal_braces = true +ij_javascript_spaces_within_object_type_braces = true +ij_javascript_spaces_within_parentheses = false +ij_javascript_spaces_within_switch_parentheses = false +ij_javascript_spaces_within_type_assertion = false +ij_javascript_spaces_within_union_types = true +ij_javascript_spaces_within_while_parentheses = false +ij_javascript_special_else_if_treatment = true +ij_javascript_ternary_operation_signs_on_next_line = true +ij_javascript_ternary_operation_wrap = on_every_item +ij_javascript_union_types_wrap = on_every_item +ij_javascript_use_chained_calls_group_indents = false +ij_javascript_use_double_quotes = false +ij_javascript_use_explicit_js_extension = global +ij_javascript_use_path_mapping = always +ij_javascript_use_public_modifier = false +ij_javascript_use_semicolon_after_statement = true +ij_javascript_var_declaration_wrap = on_every_item +ij_javascript_while_brace_force = always +ij_javascript_while_on_new_line = false +ij_javascript_wrap_comments = false + +[{*.cjsx,*.coffee}] +indent_size = 2 +tab_width = 2 +ij_continuation_indent_size = 2 +ij_coffeescript_align_function_body = false +ij_coffeescript_align_imports = false +ij_coffeescript_align_multiline_array_initializer_expression = true +ij_coffeescript_align_multiline_parameters = true +ij_coffeescript_align_multiline_parameters_in_calls = false +ij_coffeescript_align_object_properties = 0 +ij_coffeescript_align_union_types = false +ij_coffeescript_align_var_statements = 0 +ij_coffeescript_array_initializer_new_line_after_left_brace = false +ij_coffeescript_array_initializer_right_brace_on_new_line = false +ij_coffeescript_array_initializer_wrap = normal +ij_coffeescript_blacklist_imports = rxjs/Rx,node_modules/**,**/node_modules/**,@angular/material,@angular/material/typings/** +ij_coffeescript_blank_lines_around_function = 1 +ij_coffeescript_call_parameters_new_line_after_left_paren = false +ij_coffeescript_call_parameters_right_paren_on_new_line = false +ij_coffeescript_call_parameters_wrap = normal +ij_coffeescript_chained_call_dot_on_new_line = true +ij_coffeescript_comma_on_new_line = false +ij_coffeescript_enforce_trailing_comma = keep +ij_coffeescript_field_prefix = _ +ij_coffeescript_file_name_style = relaxed +ij_coffeescript_force_quote_style = false +ij_coffeescript_force_semicolon_style = false +ij_coffeescript_function_expression_brace_style = end_of_line +ij_coffeescript_import_merge_members = global +ij_coffeescript_import_prefer_absolute_path = global +ij_coffeescript_import_sort_members = true +ij_coffeescript_import_sort_module_name = false +ij_coffeescript_import_use_node_resolution = true +ij_coffeescript_imports_wrap = on_every_item +ij_coffeescript_indent_chained_calls = true +ij_coffeescript_indent_package_children = 0 +ij_coffeescript_jsx_attribute_value = braces +ij_coffeescript_keep_blank_lines_in_code = 2 +ij_coffeescript_keep_first_column_comment = true +ij_coffeescript_keep_indents_on_empty_lines = false +ij_coffeescript_keep_line_breaks = true +ij_coffeescript_keep_simple_methods_in_one_line = false +ij_coffeescript_method_parameters_new_line_after_left_paren = false +ij_coffeescript_method_parameters_right_paren_on_new_line = false +ij_coffeescript_method_parameters_wrap = off +ij_coffeescript_object_literal_wrap = on_every_item +ij_coffeescript_prefer_as_type_cast = false +ij_coffeescript_prefer_explicit_types_function_expression_returns = false +ij_coffeescript_prefer_explicit_types_function_returns = false +ij_coffeescript_prefer_explicit_types_vars_fields = false +ij_coffeescript_reformat_c_style_comments = false +ij_coffeescript_space_after_comma = true +ij_coffeescript_space_after_dots_in_rest_parameter = false +ij_coffeescript_space_after_generator_mult = true +ij_coffeescript_space_after_property_colon = true +ij_coffeescript_space_after_type_colon = true +ij_coffeescript_space_after_unary_not = false +ij_coffeescript_space_before_async_arrow_lparen = true +ij_coffeescript_space_before_class_lbrace = true +ij_coffeescript_space_before_comma = false +ij_coffeescript_space_before_function_left_parenth = true +ij_coffeescript_space_before_generator_mult = false +ij_coffeescript_space_before_property_colon = false +ij_coffeescript_space_before_type_colon = false +ij_coffeescript_space_before_unary_not = false +ij_coffeescript_spaces_around_additive_operators = true +ij_coffeescript_spaces_around_arrow_function_operator = true +ij_coffeescript_spaces_around_assignment_operators = true +ij_coffeescript_spaces_around_bitwise_operators = true +ij_coffeescript_spaces_around_equality_operators = true +ij_coffeescript_spaces_around_logical_operators = true +ij_coffeescript_spaces_around_multiplicative_operators = true +ij_coffeescript_spaces_around_relational_operators = true +ij_coffeescript_spaces_around_shift_operators = true +ij_coffeescript_spaces_around_unary_operator = false +ij_coffeescript_spaces_within_array_initializer_braces = false +ij_coffeescript_spaces_within_array_initializer_brackets = false +ij_coffeescript_spaces_within_imports = false +ij_coffeescript_spaces_within_index_brackets = false +ij_coffeescript_spaces_within_interpolation_expressions = false +ij_coffeescript_spaces_within_method_call_parentheses = false +ij_coffeescript_spaces_within_method_parentheses = false +ij_coffeescript_spaces_within_object_braces = false +ij_coffeescript_spaces_within_object_literal_braces = false +ij_coffeescript_spaces_within_object_type_braces = true +ij_coffeescript_spaces_within_range_brackets = false +ij_coffeescript_spaces_within_type_assertion = false +ij_coffeescript_spaces_within_union_types = true +ij_coffeescript_union_types_wrap = on_every_item +ij_coffeescript_use_chained_calls_group_indents = false +ij_coffeescript_use_double_quotes = true +ij_coffeescript_use_explicit_js_extension = global +ij_coffeescript_use_path_mapping = always +ij_coffeescript_use_public_modifier = false +ij_coffeescript_use_semicolon_after_statement = false +ij_coffeescript_var_declaration_wrap = normal + +[{*.har,*.jsb2,*.jsb3,*.json,.babelrc,.eslintrc,.prettierrc,.stylelintrc,bowerrc,jest.config}] +indent_size = 2 +ij_json_keep_blank_lines_in_code = 0 +ij_json_keep_indents_on_empty_lines = false +ij_json_keep_line_breaks = true +ij_json_space_after_colon = true +ij_json_space_after_comma = true +ij_json_space_before_colon = true +ij_json_space_before_comma = false +ij_json_spaces_within_braces = false +ij_json_spaces_within_brackets = false +ij_json_wrap_long_lines = false + +[{*.htm,*.html,*.ng,*.sht,*.shtm,*.shtml}] +indent_size = 2 +tab_width = 2 +ij_continuation_indent_size = 2 +ij_visual_guides = 80 +ij_html_add_new_line_before_tags = body,div,p,form,h1,h2,h3 +ij_html_align_attributes = true +ij_html_align_text = false +ij_html_attribute_wrap = on_every_item +ij_html_block_comment_at_first_column = true +ij_html_do_not_align_children_of_min_lines = 0 +ij_html_do_not_break_if_inline_tags = title,h1,h2,h3,h4,h5,h6,p +ij_html_do_not_indent_children_of_tags = html,body,thead,tbody,tfoot +ij_html_enforce_quotes = true +ij_html_inline_tags = a,abbr,acronym,b,basefont,bdo,big,br,cite,cite,code,dfn,em,font,i,img,input,kbd,label,q,s,samp,select,small,span,strike,strong,sub,sup,textarea,tt,u,var +ij_html_keep_blank_lines = 1 +ij_html_keep_indents_on_empty_lines = false +ij_html_keep_line_breaks = true +ij_html_keep_line_breaks_in_text = true +ij_html_keep_whitespaces = false +ij_html_keep_whitespaces_inside = span,pre,textarea +ij_html_line_comment_at_first_column = true +ij_html_new_line_after_last_attribute = when multiline +ij_html_new_line_before_first_attribute = when multiline +ij_html_quote_style = double +ij_html_remove_new_line_before_tags = br +ij_html_space_after_tag_name = false +ij_html_space_around_equality_in_attribute = false +ij_html_space_inside_empty_tag = true +ij_html_text_wrap = off + +[{*.markdown,*.md,migrate-to-nextjs.md}] +ij_markdown_force_one_space_after_blockquote_symbol = true +ij_markdown_force_one_space_after_header_symbol = true +ij_markdown_force_one_space_after_list_bullet = true +ij_markdown_force_one_space_between_words = true +ij_markdown_keep_indents_on_empty_lines = false +ij_markdown_max_lines_around_block_elements = 1 +ij_markdown_max_lines_around_header = 1 +ij_markdown_max_lines_between_paragraphs = 1 +ij_markdown_min_lines_around_block_elements = 1 +ij_markdown_min_lines_around_header = 1 +ij_markdown_min_lines_between_paragraphs = 1 + +[{*.yaml,*.yml}] +indent_size = 2 +ij_yaml_align_values_properties = do_not_align +ij_yaml_autoinsert_sequence_marker = true +ij_yaml_block_mapping_on_new_line = false +ij_yaml_indent_sequence_value = true +ij_yaml_keep_indents_on_empty_lines = false +ij_yaml_keep_line_breaks = true +ij_yaml_sequence_on_new_line = false +ij_yaml_space_before_colon = false +ij_yaml_spaces_within_braces = true +ij_yaml_spaces_within_brackets = true diff --git a/.vscode/settings.json b/.vscode/settings.json index 413e1c6..dd0b2a2 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -8,4 +8,4 @@ "editor.tabSize": 2, "typescript.tsdk": "node_modules/typescript/lib", "typescript.enablePromptUseWorkspaceTsdk": true -} \ No newline at end of file +} diff --git a/src/constants.ts b/src/constants.ts index cd7f874..c98f38b 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -5,7 +5,6 @@ const defaultConfig = { errorSinkEndpoint: '/errors', configFetchEndpoint: '/config', allowLocalUrls: false, - keysToRedact: [], ignoredDomains: [], // After the close command is sent, wait for this many milliseconds before diff --git a/src/index.ts b/src/index.ts index 7e9fbe8..281e8d7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -114,19 +114,11 @@ const Supergood = () => { ...supergoodConfig, remoteConfig: processRemoteConfig(remoteConfigPayload) }; - if (supergoodConfig.remoteConfig && !interceptorWasInitialized) { - interceptor.setup(); - interceptorWasInitialized = true; - log.debug('Ready to intercept requests', { config: supergoodConfig }); - } } catch (e) { log.error(errors.FETCHING_CONFIG, { config: supergoodConfig }, e as Error) } }; - // Fetch and process remote config upon initialization - // Also start up the interception if a remote config is present - // await fetchAndProcessRemoteConfig(); const initializeInterceptors = () => { interceptor.setup(); @@ -218,9 +210,19 @@ const Supergood = () => { } ); + // Fetch the initial config and process it + await fetchAndProcessRemoteConfig(); + initializeInterceptors(); + + // Fetch the config ongoing every milliseconds + remoteConfigFetchInterval = setInterval(fetchAndProcessRemoteConfig, supergoodConfig.remoteConfigFetchInterval); + // Flushes the cache every milliseconds flushInterval = setInterval(flushCache, supergoodConfig.flushInterval); + + // https://httptoolkit.com/blog/unblocking-node-with-unref/ flushInterval.unref(); + remoteConfigFetchInterval.unref(); }; const cacheRequest = async (request: RequestType, baseUrl: string) => { diff --git a/src/types.ts b/src/types.ts index 23b3792..5a6bba5 100644 --- a/src/types.ts +++ b/src/types.ts @@ -5,7 +5,14 @@ interface HeaderOptionType { }; } -type BodyType = Record; +type JSONValue = string | number | boolean | null | JSONArray | JSONObject; + +interface JSONArray extends Array {} +interface JSONObject { + [key: string]: JSONValue; +} + +type BodyType = JSONObject interface RequestType { id: string; @@ -101,6 +108,30 @@ interface LoggerType { debug: (message: string, payload?: any) => void; } +type RemoteConfigPayloadType = Array<{ + domain: string; + endpoints: Array<{ + name: string; + matchingRegex: { + regex: string; + location: string; + }; + endpointConfiguration: { + action: string; + sensitiveKeys: Array< + { + keyPath: string; + }>; + } + }>; +}>; + +type SensitiveKeyMetadata = { + keyPath?: string; + length?: number; + type?: string; +}; + export type { HeaderOptionType, RequestType, @@ -111,7 +142,8 @@ export type { ConfigType, ErrorPayloadType, BodyType, - MetadataType, + SensitiveKeyMetadata, RemoteConfigType, - EndpointConfigType + EndpointConfigType, + RemoteConfigPayloadType, }; diff --git a/src/utils.test.ts b/src/utils.test.ts new file mode 100644 index 0000000..cff4978 --- /dev/null +++ b/src/utils.test.ts @@ -0,0 +1,398 @@ +import { RequestType, ResponseType } from './types'; +import { prepareData, expandSensitiveKeySetForArrays, redactValuesFromKeys } from './utils'; +import _get from 'lodash.get'; + +it('generates multiple sensitive key paths for an array', () => { + const obj = { + blog: { + name: 'My Blog', + posts: [ + { + id: 1, + title: 'json-server', + author: 'typicode' + }, + { + id: 2, + title: 'nodejs', + author: 'alex' + }, + { + id: 3, + title: 'typescript', + author: 'zack' + }, + { + id: 4, + title: 'python', + author: 'steve' + } + ] + } + }; + const sensitiveKeys = ['blog.posts[].title']; + expect(expandSensitiveKeySetForArrays(obj, sensitiveKeys)).toEqual([ + 'blog.posts[0].title', + 'blog.posts[1].title', + 'blog.posts[2].title', + 'blog.posts[3].title' + ]); +}); + +it('generates multiple sensitive key paths for an object with nested arrays', () => { + const obj = { + blog: { + name: 'My Blog', + posts: [ + { + id: 1, + title: 'json-server', + author: 'typicode', + comments: [ + { + id: 1, + body: 'some comment', + postId: 1 + }, + { + id: 2, + body: 'some comment', + postId: 1 + }, + { + id: 3, + body: 'some comment', + postId: 1 + }, + { + id: 4, + body: 'some comment', + postId: 1 + } + ] + }, + { + id: 2, + title: 'nodejs', + author: 'alex', + comments: [ + { + id: 1, + body: 'some comment', + postId: 1 + }, + { + id: 2, + body: 'some comment', + postId: 1 + }, + { + id: 3, + body: 'some comment', + postId: 1 + } + ] + }, + { + id: 3, + title: 'typescript', + author: 'zack', + comments: [ + { + id: 1, + body: 'some comment', + postId: 1 + }, + { + id: 2, + body: 'some comment', + postId: 1 + } + ] + }, + { + id: 4, + title: 'python', + author: 'steve', + comments: [ + { + id: 1, + body: 'some comment', + postId: 1 + } + ] + } + ] + } + }; + const sensitiveKeys = ['blog.posts[].comments[].body']; + expect(expandSensitiveKeySetForArrays(obj, sensitiveKeys)).toEqual([ + 'blog.posts[0].comments[0].body', + 'blog.posts[0].comments[1].body', + 'blog.posts[0].comments[2].body', + 'blog.posts[0].comments[3].body', + 'blog.posts[1].comments[0].body', + 'blog.posts[1].comments[1].body', + 'blog.posts[1].comments[2].body', + 'blog.posts[2].comments[0].body', + 'blog.posts[2].comments[1].body', + 'blog.posts[3].comments[0].body' + ]); +}); + +it('redacts values from keys with proper marshalling', () => { + const MOCK_DATA_SERVER = 'http://localhost:3001'; + const obj = { + request: { + id: '', + headers: {}, + method: 'GET', + url: `${MOCK_DATA_SERVER}/posts`, + path: '/posts', + search: '', + requestedAt: new Date(), + body: { + name: 'My Blog', + posts: [ + { + id: 1, + title: 'json-server', + author: 'typicode' + }, + { + id: 2, + title: 'nodejs', + author: 'alex' + }, + { + id: 3, + title: 'typescript', + author: 'zack' + }, + { + id: 4, + title: 'python', + author: 'steve' + } + ] + } + }, + }; + + const remoteConfig = { + [new URL(MOCK_DATA_SERVER).hostname]: { + '/posts': { + location: 'path', + regex: '/posts', + ignored: false, + sensitiveKeys: ['request_body.posts[].title'] + } + } + }; + + const redactedObj = redactValuesFromKeys(obj, remoteConfig); + expect(_get(redactedObj, 'event.request.body.posts[0].title')).toBeNull(); + expect(redactedObj.sensitiveKeyMetadata[0]).toEqual({ + keyPath: "request_body.posts[0].title", + type: "string", + length: 11, + }) +}); + +it('redacts values from keys of nested array', () => { + const MOCK_DATA_SERVER = 'http://localhost:3001'; + const obj = { + request: { + id: '', + headers: {}, + method: 'GET', + url: `${MOCK_DATA_SERVER}/posts`, + path: '/posts', + search: '', + requestedAt: new Date(), + body: { + name: 'My Blog', + posts: [ + { + id: 1, + title: 'json-server', + author: 'typicode', + comments: [ + { + id: 1, + body: 'some comment', + postId: 1 + }, + { + id: 2, + body: 'some comment', + postId: 1 + }, + { + id: 3, + body: 'some comment', + postId: 1 + }, + { + id: 4, + body: 'some comment', + postId: 1 + } + ] + }, + { + id: 2, + title: 'nodejs', + author: 'alex', + comments: [ + { + id: 1, + body: 'some comment', + postId: 1 + }, + { + id: 2, + body: 'some comment', + postId: 1 + }, + { + id: 3, + body: 'some comment', + postId: 1 + } + ] + }, + { + id: 3, + title: 'typescript', + author: 'zack', + comments: [ + { + id: 1, + body: 'some comment', + postId: 1 + }, + { + id: 2, + body: 'some comment', + postId: 1 + } + ] + }, + { + id: 4, + title: 'python', + author: 'steve', + comments: [ + { + id: 1, + body: 'some comment', + postId: 1 + } + ] + } + ] + } + }, + }; + + const remoteConfig = { + [new URL(MOCK_DATA_SERVER).hostname]: { + '/posts': { + location: 'path', + regex: '/posts', + ignored: false, + sensitiveKeys: ['request_body.posts[].comments[].body'] + } + } + }; + + const redactedObj = redactValuesFromKeys(obj, remoteConfig); + expect(_get(redactedObj, 'event.request.body.posts[0].comments[0].body')).toBeNull(); + expect(redactedObj.sensitiveKeyMetadata[0]).toEqual({ + keyPath: "request_body.posts[0].comments[0].body", + type: "string", + length: 12, + }) +}); + +it('will not blow up or redact anything if the sensitive key is bad', () => { + const MOCK_DATA_SERVER = 'http://localhost:3001'; + const obj = { + request: { + id: '', + headers: {}, + method: 'GET', + url: `${MOCK_DATA_SERVER}/posts`, + path: '/posts', + search: '', + requestedAt: new Date(), + body: { + name: 'My Blog', + comments: [ + 1,2,3,4 + ] + } + }, + }; + + const remoteConfig = { + [new URL(MOCK_DATA_SERVER).hostname]: { + '/posts': { + location: 'path', + regex: '/posts', + ignored: false, + sensitiveKeys: ['request_body.posts[].title[]'] + } + } + }; + + const redactedObj = redactValuesFromKeys(obj, remoteConfig); + expect(_get(redactedObj, 'event.request.body.name')).toBeTruthy(); + expect(redactedObj.sensitiveKeyMetadata.length).toEqual(0) +}); + + +it('will prepare the data appropriately for posting to the server', () => { + const MOCK_DATA_SERVER = 'http://localhost:3001'; + const obj = { + request: { + id: '', + headers: {}, + method: 'GET', + url: `${MOCK_DATA_SERVER}/posts`, + path: '/posts', + search: '', + requestedAt: new Date(), + body: {}, + }, + response: { + headers: {}, + status: 200, + statusText: 'OK', + respondedAt: new Date(), + body: { + name: 'My Blog', + comments: [ + 1,2,3,4 + ] + } + } + }; + + const remoteConfig = { + [new URL(MOCK_DATA_SERVER).hostname]: { + '/posts': { + location: 'path', + regex: '/posts', + ignored: false, + sensitiveKeys: ['response_body.comments'] + } + } + }; + + const events = prepareData([obj], remoteConfig); + console.log(JSON.stringify(events, null, 2)) + expect(_get(events[0], 'response.body.name')).toBeTruthy(); + expect(events[0].metadata.sensitiveKeys.length).toEqual(1) +}); diff --git a/src/utils.ts b/src/utils.ts index bc79900..0bad0d0 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -5,11 +5,11 @@ import { ResponseType, EventRequestType, ErrorPayloadType, - ConfigType, + RemoteConfigPayloadType, RemoteConfigType, - EndpointConfigType + EndpointConfigType, + SensitiveKeyMetadata } from './types'; -import crypto from 'node:crypto'; import { postError } from './api'; import { name, version } from '../package.json'; import https from 'https'; @@ -86,32 +86,89 @@ const getHeaderOptions = ( }; }; -const marshalKeypath = (keypath: string) => { - const [first] = keypath.split('.'); - if(first === 'request_headers') return keypath.replace('request_headers', 'request.headers'); - if(first === 'request_body') return keypath.replace('request_body', 'request.body'); - if(first === 'response_headers') return keypath.replace('response_headers', 'response.headers'); - if(first === 'response_body') return keypath.replace('response_body', 'response.body'); +const marshalKeyPath = (keypath: string) => { + if(/^request_headers/.test(keypath)) return keypath.replace('request_headers', 'request.headers'); + if(/^request_body/.test(keypath)) return keypath.replace('request_body', 'request.body'); + if(/^response_headers/.test(keypath)) return keypath.replace('response_headers', 'response.headers'); + if(/^response_body/.test(keypath)) return keypath.replace('response_body', 'response.body'); return keypath; } +const unmarshalKeyPath = (keypath: string) => { + if(/^request\.headers/.test(keypath)) return keypath.replace('request.headers', 'request_headers'); + if(/^request\.body/.test(keypath)) return keypath.replace('request.body', 'request_body'); + if(/^response\.headers/.test(keypath)) return keypath.replace('response.headers', 'response_headers'); + if(/^response\.body/.test(keypath)) return keypath.replace('response.body', 'response_body'); + return keypath; +} + +const expandSensitiveKeySetForArrays = (obj: any, sensitiveKeys: Array): Array => { + const expandKey = (key: string, obj: any): Array => { + // Split the key by dots, considering the array brackets as part of the key + const parts = key.match(/[^.\[\]]+|\[\d*\]|\[\*\]/g) || []; + + // Recursively expand the key + return expand(parts, obj, ''); + }; + + const expand = (parts: string[], obj: any, keyPath: string): Array => { + const path = keyPath; + if (parts.length === 0) { + return [path]; // Remove trailing dot + } + const part = parts[0]; + const isProperty = !part.startsWith('['); + const separator = path && isProperty ? '.' : ''; + + // Check for array notations + if (/\[\*?\]/.test(part)) { + if (!Array.isArray(obj)) { + return []; + } + // Expand for each element in the array + return obj.flatMap((_, index) => + expand(parts.slice(1), obj[index], `${path}${separator}[${index}]`) + ); + } else if (part.startsWith('[') && part.endsWith(']')) { + // Specific index in the array + const index = parseInt(part.slice(1, -1), 10); + if (!isNaN(index) && index < obj.length) { + return expand(parts.slice(1), obj[index], `${path}${separator}${part}`); + } else { + return []; + } + } else { + // Regular object property + if (obj && typeof obj === 'object' && part in obj) { + return expand(parts.slice(1), obj[part], `${path}${separator}${part}`); + } else { + return []; + } + } + }; + + return sensitiveKeys.flatMap(key => expandKey(key, obj)); +}; + const redactValuesFromKeys = ( - obj: { request?: RequestType; response?: ResponseType }, + event: { request?: RequestType; response?: ResponseType }, remoteConfig: RemoteConfigType -) => { - const endpointConfig = getEndpointConfigForRequest(obj.request as RequestType, remoteConfig); - if (!endpointConfig || !endpointConfig?.sensitiveKeys?.length) return obj; +): { event: { request?: RequestType; response?: ResponseType }, sensitiveKeyMetadata: Array } => { + let sensitiveKeyMetadata: Array = []; + const endpointConfig = getEndpointConfigForRequest(event.request as RequestType, remoteConfig); + if (!endpointConfig || !endpointConfig?.sensitiveKeys?.length) return { event, sensitiveKeyMetadata }; else { - const sensitiveKeys = endpointConfig.sensitiveKeys; - let objCopy = { ...obj }; + const sensitiveKeys = expandSensitiveKeySetForArrays(event, endpointConfig.sensitiveKeys.map(key => marshalKeyPath(key))) for (let i = 0; i < sensitiveKeys.length; i++) { - const keyPath = marshalKeypath(sensitiveKeys[i]); - const value = _get(objCopy, keyPath); + const keyPath = sensitiveKeys[i]; + // Add sensitive key for array expansion + const value = _get(event, keyPath); if (value) { - objCopy = _set(objCopy, keyPath, redactValue(value)); + _set(event, keyPath, null); + sensitiveKeyMetadata.push({ keyPath: unmarshalKeyPath(keyPath), ...redactValue(value) }); } } - return objCopy; + return { event, sensitiveKeyMetadata }; } }; @@ -150,14 +207,20 @@ const redactValue = ( dataLength = 1; dataType = 'boolean'; } - return `redacted:${dataLength}:${dataType}` + return { length: dataLength, type: dataType }; }; const prepareData = ( events: Array, remoteConfig: RemoteConfigType, ) => { - return events.filter((e) => redactValuesFromKeys(e, remoteConfig)); + return events.map((e) => { + const { event, sensitiveKeyMetadata } = redactValuesFromKeys(e, remoteConfig); + return ({ + ...event, + metadata: { sensitiveKeys: sensitiveKeyMetadata } + }) + }) }; const post = ( @@ -265,26 +328,7 @@ const get = ( }); } -type RemoteConfigPayload = Array<{ - domain: string; - endpoints: Array<{ - name: string; - matchingRegex: { - regex: string; - location: string; - }; - endpointConfiguration: { - action: string; - sensitiveKeys: Array< - { - keyPath: string; - }>; - } - }>; -}>; - - -const processRemoteConfig = (remoteConfigPayload: RemoteConfigPayload) => { +const processRemoteConfig = (remoteConfigPayload: RemoteConfigPayloadType) => { return (remoteConfigPayload || []).reduce((remoteConfig, domainConfig) => { const { domain, endpoints } = domainConfig; const endpointConfig = endpoints.reduce((endpointConfig, endpoint) => { @@ -321,11 +365,13 @@ const getStrRepresentationFromPath = (request: RequestType, location: string) => const getEndpointConfigForRequest = (request: RequestType, remoteConfig: RemoteConfigType) => { const domains = Object.keys(remoteConfig); const domain = domains.find((domain) => request.url.includes(domain)); + // If the domain doesn't exist in the config, then we return nothing if (!domain) return null; const endpointConfigs = remoteConfig[domain]; + for (let i = 0; i < Object.keys(endpointConfigs).length; i++) { - const endpointConfig = endpointConfigs[i]; + const endpointConfig = endpointConfigs[Object.keys(endpointConfigs)[i]]; const { regex, location } = endpointConfig; const regexObj = new RegExp(regex); const strRepresentation = getStrRepresentationFromPath(request, location); @@ -351,5 +397,6 @@ export { sleep, post, get, - getEndpointConfigForRequest + getEndpointConfigForRequest, + expandSensitiveKeySetForArrays }; diff --git a/test/consts.ts b/test/consts.ts index 8892086..692b440 100644 --- a/test/consts.ts +++ b/test/consts.ts @@ -9,10 +9,12 @@ export const SUPERGOOD_SERVER = `http://localhost:${SUPERGOOD_SERVER_PORT}`; export const SUPERGOOD_CONFIG = { flushInterval: 30000, + remoteConfigFetchInterval: 10000, cacheTtl: 0, eventSinkEndpoint: `/events`, errorSinkEndpoint: `/errors`, - keysToHash: ['request.body', 'response.body'], + configFetchEndpoint: '/config', + allowLocalUrls: true, ignoredDomains: [] }; diff --git a/test/e2e/core.e2e.test.ts b/test/e2e/core.e2e.test.ts index ab6668e..38eca88 100644 --- a/test/e2e/core.e2e.test.ts +++ b/test/e2e/core.e2e.test.ts @@ -32,7 +32,6 @@ describe('core functionality', () => { }, SUPERGOOD_SERVER ); - const numberOfHttpCalls = 5; for (let i = 0; i < numberOfHttpCalls; i++) { await axios.get(`${MOCK_DATA_SERVER}/posts`); diff --git a/test/e2e/remote-config.e2e.test.ts b/test/e2e/remote-config.e2e.test.ts new file mode 100644 index 0000000..45ec2b1 --- /dev/null +++ b/test/e2e/remote-config.e2e.test.ts @@ -0,0 +1,141 @@ +import fetch from 'node-fetch'; +import Supergood from '../../src'; +import { + MOCK_DATA_SERVER, + SUPERGOOD_CLIENT_ID, + SUPERGOOD_CLIENT_SECRET, + SUPERGOOD_CONFIG, + SUPERGOOD_SERVER +} from '../consts'; +import { RemoteConfigPayloadType } from '../../src/types'; +import { getEvents } from '../utils/function-call-args'; +import { mockApi } from '../utils/mock-api'; + +describe('remote config functionality', () => { + + it('fetches remote config', async () => { + const fetchRemoteConfigResponse = [] as RemoteConfigPayloadType; + const { postEventsMock } = mockApi({ fetchRemoteConfigResponse }); + await Supergood.init( + { + config: { ...SUPERGOOD_CONFIG, allowLocalUrls: true }, + clientId: SUPERGOOD_CLIENT_ID, + clientSecret: SUPERGOOD_CLIENT_SECRET + }, + SUPERGOOD_SERVER + ); + await fetch(`${MOCK_DATA_SERVER}/posts`); + await Supergood.close(); + expect(getEvents(postEventsMock).length).toEqual(1); + }) + + it('fetches remote config and ignores some endpoints', async () => { + const fetchRemoteConfigResponse = [{ + domain: new URL(MOCK_DATA_SERVER).hostname, + endpoints: [{ + name: '/posts', + matchingRegex: { + regex: '/posts', + location: 'path' + }, + endpointConfiguration: { + action: 'Ignore', + sensitiveKeys: [] + } + }] + } + ] as RemoteConfigPayloadType; + const { postEventsMock } = mockApi({ fetchRemoteConfigResponse }); + await Supergood.init( + { + config: { ...SUPERGOOD_CONFIG, allowLocalUrls: true }, + clientId: SUPERGOOD_CLIENT_ID, + clientSecret: SUPERGOOD_CLIENT_SECRET + }, + SUPERGOOD_SERVER + ); + await fetch(`${MOCK_DATA_SERVER}/posts`); + await fetch(`${MOCK_DATA_SERVER}/gzipped-response`); + await Supergood.close(); + const eventsPosted = getEvents(postEventsMock); + expect(eventsPosted.length).toEqual(1); + }) + + it('fetches remote config and redacts sensitive keys', async () => { + const fetchRemoteConfigResponse = [{ + domain: new URL(MOCK_DATA_SERVER).hostname, + endpoints: [{ + name: '/profile', + matchingRegex: { + regex: '/profile', + location: 'path' + }, + endpointConfiguration: { + action: 'Allow', + sensitiveKeys: [{ + keyPath: 'response_body.name' + }] + } + }] + }] as RemoteConfigPayloadType; + const { postEventsMock } = mockApi({ fetchRemoteConfigResponse }); + await Supergood.init( + { + config: { ...SUPERGOOD_CONFIG, allowLocalUrls: true }, + clientId: SUPERGOOD_CLIENT_ID, + clientSecret: SUPERGOOD_CLIENT_SECRET + }, + SUPERGOOD_SERVER + ); + await fetch(`${MOCK_DATA_SERVER}/profile`); + await Supergood.close(); + const eventsPosted = getEvents(postEventsMock); + expect(eventsPosted.length).toEqual(1); + expect((eventsPosted[0]?.response?.body as any).name).toEqual('redacted:8:string') + }) + + it('fetches remote config and redacts sensitive keys within an array', async () => { + const fetchRemoteConfigResponse = [{ + domain: new URL(MOCK_DATA_SERVER).hostname, + endpoints: [{ + name: '/posts', + matchingRegex: { + regex: '/posts', + location: 'path' + }, + endpointConfiguration: { + action: 'Allow', + sensitiveKeys: [{ + keyPath: 'response_body[0].title' + }] + } + }] + }] as RemoteConfigPayloadType; + const { postEventsMock } = mockApi({ fetchRemoteConfigResponse }); + await Supergood.init( + { + config: { ...SUPERGOOD_CONFIG, allowLocalUrls: true }, + clientId: SUPERGOOD_CLIENT_ID, + clientSecret: SUPERGOOD_CLIENT_SECRET + }, + SUPERGOOD_SERVER + ); + await fetch(`${MOCK_DATA_SERVER}/posts`); + await Supergood.close(); + const eventsPosted = getEvents(postEventsMock); + console.log(eventsPosted[0]?.response?.body) + expect(eventsPosted.length).toEqual(1); + }) + + it('fetches remote config with invalid sensitive key format, logs an error but continues on', () => { + expect(true).toBeTruthy(); + }) + + it('fetches remote config and redacts sensitive keys within an array which is nested within an array', () => { + expect(true).toBeTruthy(); + }) + + it('does not intercept anything if the remote config can not be fetched', () => { + expect(true).toBeTruthy(); + }) +}); diff --git a/test/mock-db.js b/test/mock-db.js index 3c2d07c..885ae4c 100644 --- a/test/mock-db.js +++ b/test/mock-db.js @@ -4,6 +4,21 @@ const db = { id: 1, title: 'json-server', author: 'typicode' + }, + { + id: 2, + title: 'nodejs', + author: 'alex' + }, + { + id: 3, + title: 'typescript', + author: 'zack' + }, + { + id: 4, + title: 'python', + author: 'steve' } ], comments: [ diff --git a/test/utils/mock-api.ts b/test/utils/mock-api.ts index 09a11ce..3a8fa95 100644 --- a/test/utils/mock-api.ts +++ b/test/utils/mock-api.ts @@ -1,15 +1,29 @@ import * as api from '../../src/api'; +import { RemoteConfigPayloadType } from '../../src/types'; + +export const mockApi = ( + { + postErrorsResponse, + postEventsResponse, + fetchRemoteConfigResponse + }: + { postErrorsResponse?: any, + postEventsResponse?: any, + fetchRemoteConfigResponse?: RemoteConfigPayloadType + } = {} +) => { -export function mockApi() { const postEventsMock = jest .spyOn(api, 'postEvents') - .mockImplementation(async (_, data) => ({ data } as any)); + .mockImplementation((async (_, data) => postEventsResponse ?? ({ data } as any))); + const postErrorMock = jest .spyOn(api, 'postError') - .mockImplementation(async (_, payload) => ({ payload } as any)); + .mockImplementation((async (_, payload) => postErrorsResponse ?? ({ payload } as any))); + const fetchRemoteConfigMock = jest .spyOn(api, 'fetchRemoteConfig') - .mockImplementation(async () => ([] as any)); + .mockImplementation((async () => fetchRemoteConfigResponse ?? ([] as any))); return { postEventsMock, postErrorMock, fetchRemoteConfigMock }; } From 6f702472412e21785452f61b947b4bc7eba9cd43 Mon Sep 17 00:00:00 2001 From: Alex Klarfeld Date: Fri, 8 Dec 2023 15:51:18 -0800 Subject: [PATCH 16/30] Fixed tests for remote config fetching --- src/types.ts | 3 +++ src/utils.test.ts | 1 - test/e2e/core.e2e.test.ts | 2 +- test/e2e/remote-config.e2e.test.ts | 4 ++-- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/types.ts b/src/types.ts index 5a6bba5..30f3c61 100644 --- a/src/types.ts +++ b/src/types.ts @@ -72,6 +72,9 @@ interface MetadataType { interface EventRequestType { request: RequestType; response: ResponseType; + metadata?: { + sensitiveKeys: Array; + }; } // interface EventResponseType {} diff --git a/src/utils.test.ts b/src/utils.test.ts index cff4978..bb5ce2f 100644 --- a/src/utils.test.ts +++ b/src/utils.test.ts @@ -392,7 +392,6 @@ it('will prepare the data appropriately for posting to the server', () => { }; const events = prepareData([obj], remoteConfig); - console.log(JSON.stringify(events, null, 2)) expect(_get(events[0], 'response.body.name')).toBeTruthy(); expect(events[0].metadata.sensitiveKeys.length).toEqual(1) }); diff --git a/test/e2e/core.e2e.test.ts b/test/e2e/core.e2e.test.ts index 38eca88..7eeb272 100644 --- a/test/e2e/core.e2e.test.ts +++ b/test/e2e/core.e2e.test.ts @@ -108,7 +108,7 @@ describe('core functionality', () => { }, SUPERGOOD_SERVER ); - axios.get(`${MOCK_DATA_SERVER}/200?sleep=2000`); + axios.get(`${MOCK_DATA_SERVER}/200?sleep=3000`); await sleep(1000); await Supergood.close(); diff --git a/test/e2e/remote-config.e2e.test.ts b/test/e2e/remote-config.e2e.test.ts index 45ec2b1..14e3ed6 100644 --- a/test/e2e/remote-config.e2e.test.ts +++ b/test/e2e/remote-config.e2e.test.ts @@ -10,6 +10,7 @@ import { import { RemoteConfigPayloadType } from '../../src/types'; import { getEvents } from '../utils/function-call-args'; import { mockApi } from '../utils/mock-api'; +import _get from 'lodash.get'; describe('remote config functionality', () => { @@ -91,7 +92,7 @@ describe('remote config functionality', () => { await Supergood.close(); const eventsPosted = getEvents(postEventsMock); expect(eventsPosted.length).toEqual(1); - expect((eventsPosted[0]?.response?.body as any).name).toEqual('redacted:8:string') + expect(_get(eventsPosted[0], 'metadata.sensitiveKeys[0].length')).toEqual(8) }) it('fetches remote config and redacts sensitive keys within an array', async () => { @@ -123,7 +124,6 @@ describe('remote config functionality', () => { await fetch(`${MOCK_DATA_SERVER}/posts`); await Supergood.close(); const eventsPosted = getEvents(postEventsMock); - console.log(eventsPosted[0]?.response?.body) expect(eventsPosted.length).toEqual(1); }) From f258f581682cc2226cbe81c5e1279d7bb0c21e94 Mon Sep 17 00:00:00 2001 From: Alex Klarfeld Date: Fri, 8 Dec 2023 15:57:48 -0800 Subject: [PATCH 17/30] Fixed tests --- test/e2e/remote-config.e2e.test.ts | 25 +++++++++++++++---------- test/utils/mock-api.ts | 8 +++++--- 2 files changed, 20 insertions(+), 13 deletions(-) diff --git a/test/e2e/remote-config.e2e.test.ts b/test/e2e/remote-config.e2e.test.ts index 14e3ed6..42456f1 100644 --- a/test/e2e/remote-config.e2e.test.ts +++ b/test/e2e/remote-config.e2e.test.ts @@ -11,6 +11,7 @@ import { RemoteConfigPayloadType } from '../../src/types'; import { getEvents } from '../utils/function-call-args'; import { mockApi } from '../utils/mock-api'; import _get from 'lodash.get'; +import { fetchRemoteConfig } from '../../src/api'; describe('remote config functionality', () => { @@ -127,15 +128,19 @@ describe('remote config functionality', () => { expect(eventsPosted.length).toEqual(1); }) - it('fetches remote config with invalid sensitive key format, logs an error but continues on', () => { - expect(true).toBeTruthy(); - }) - - it('fetches remote config and redacts sensitive keys within an array which is nested within an array', () => { - expect(true).toBeTruthy(); - }) - - it('does not intercept anything if the remote config can not be fetched', () => { - expect(true).toBeTruthy(); + it('does not intercept anything if the remote config can not be fetched', async () => { + const fetchRemoteConfigFunction = () => { throw new Error('Cant fetch remote config') }; + const { postEventsMock } = mockApi({ fetchRemoteConfigFunction }); + await Supergood.init( + { + config: { ...SUPERGOOD_CONFIG, allowLocalUrls: true }, + clientId: SUPERGOOD_CLIENT_ID, + clientSecret: SUPERGOOD_CLIENT_SECRET + }, + SUPERGOOD_SERVER + ); + await fetch(`${MOCK_DATA_SERVER}/posts`); + await Supergood.close(); + expect(postEventsMock).toHaveBeenCalledTimes(0); }) }); diff --git a/test/utils/mock-api.ts b/test/utils/mock-api.ts index 3a8fa95..00d4300 100644 --- a/test/utils/mock-api.ts +++ b/test/utils/mock-api.ts @@ -5,11 +5,13 @@ export const mockApi = ( { postErrorsResponse, postEventsResponse, - fetchRemoteConfigResponse + fetchRemoteConfigResponse, + fetchRemoteConfigFunction, }: { postErrorsResponse?: any, postEventsResponse?: any, - fetchRemoteConfigResponse?: RemoteConfigPayloadType + fetchRemoteConfigResponse?: RemoteConfigPayloadType, + fetchRemoteConfigFunction?: () => Promise, } = {} ) => { @@ -23,7 +25,7 @@ export const mockApi = ( const fetchRemoteConfigMock = jest .spyOn(api, 'fetchRemoteConfig') - .mockImplementation((async () => fetchRemoteConfigResponse ?? ([] as any))); + .mockImplementation(fetchRemoteConfigFunction ?? (async () => fetchRemoteConfigResponse ?? ([] as any))); return { postEventsMock, postErrorMock, fetchRemoteConfigMock }; } From be7d061f844a71691c98287ab729e327c287143a Mon Sep 17 00:00:00 2001 From: Alex Klarfeld Date: Fri, 8 Dec 2023 16:08:22 -0800 Subject: [PATCH 18/30] Prematurely added metadata --- src/index.ts | 6 +++++- src/types.ts | 1 + 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index 281e8d7..a6139ee 100644 --- a/src/index.ts +++ b/src/index.ts @@ -16,7 +16,8 @@ import { EventRequestType, ConfigType, LoggerType, - RequestType + RequestType, + MetadataType } from './types'; import { defaultConfig, @@ -39,6 +40,7 @@ const Supergood = () => { let headerOptions: HeaderOptionType; let supergoodConfig: ConfigType; + let supergoodMetadata: MetadataType; let requestCache: NodeCache; let responseCache: NodeCache; @@ -61,6 +63,7 @@ const Supergood = () => { clientId?: string; clientSecret?: string; config?: Partial; + metadata?: Partial; } = { clientId: process.env.SUPERGOOD_CLIENT_ID as string, clientSecret: process.env.SUPERGOOD_CLIENT_SECRET as string, @@ -80,6 +83,7 @@ const Supergood = () => { ...defaultConfig, ...config } as ConfigType; + supergoodMetadata = metadata as MetadataType; requestCache = new NodeCache({ stdTTL: 0 diff --git a/src/types.ts b/src/types.ts index 30f3c61..badd8aa 100644 --- a/src/types.ts +++ b/src/types.ts @@ -149,4 +149,5 @@ export type { RemoteConfigType, EndpointConfigType, RemoteConfigPayloadType, + MetadataType }; From b0d1007a4a8a2dbe51870a3df4676249b2d2de67 Mon Sep 17 00:00:00 2001 From: Alex Klarfeld Date: Fri, 8 Dec 2023 16:34:18 -0800 Subject: [PATCH 19/30] integrated metadata changes --- src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index a6139ee..14b968f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -285,7 +285,7 @@ const Supergood = () => { if (error.message === errors.UNAUTHORIZED) { log.error( errors.UNAUTHORIZED, - { config: supergoodConfig }, + { config: supergoodConfig, metadata: { ...supergoodMetadata }}, error, { reportOut: false From 65c838d81ac53f8cb111e1933195fbd435c29437 Mon Sep 17 00:00:00 2001 From: Alex Klarfeld Date: Fri, 8 Dec 2023 17:33:43 -0800 Subject: [PATCH 20/30] fixed comma --- test/jest.e2e.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/jest.e2e.config.js b/test/jest.e2e.config.js index 2ec00a1..c058e1e 100644 --- a/test/jest.e2e.config.js +++ b/test/jest.e2e.config.js @@ -10,5 +10,5 @@ module.exports = { '^.+\\.(js)$': 'babel-jest' }, transformIgnorePatterns: [], - setupFilesAfterEnv: ['./setupTests.ts'], + setupFilesAfterEnv: ['./setupTests.ts'] }; From b94506361981c0396e7ead4e6aee76c54a839700 Mon Sep 17 00:00:00 2001 From: Alex Klarfeld Date: Tue, 12 Dec 2023 08:18:24 -0800 Subject: [PATCH 21/30] changed blob to v8 --- src/constants.ts | 2 +- src/index.ts | 16 +++++++++------- src/utils.ts | 2 +- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/constants.ts b/src/constants.ts index c98f38b..a711360 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -3,7 +3,7 @@ const defaultConfig = { remoteConfigFetchInterval: 10000, eventSinkEndpoint: '/events', errorSinkEndpoint: '/errors', - configFetchEndpoint: '/config', + remoteConfigFetchEndpoint: '/config', allowLocalUrls: false, ignoredDomains: [], diff --git a/src/index.ts b/src/index.ts index 14b968f..cfcf18c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -10,7 +10,7 @@ import { getEndpointConfigForRequest } from './utils'; import { postEvents, fetchRemoteConfig } from './api'; - +import v8 from 'v8'; import { HeaderOptionType, EventRequestType, @@ -220,13 +220,12 @@ const Supergood = () => { // Fetch the config ongoing every milliseconds remoteConfigFetchInterval = setInterval(fetchAndProcessRemoteConfig, supergoodConfig.remoteConfigFetchInterval); + remoteConfigFetchInterval.unref(); // Flushes the cache every milliseconds flushInterval = setInterval(flushCache, supergoodConfig.flushInterval); - // https://httptoolkit.com/blog/unblocking-node-with-unref/ flushInterval.unref(); - remoteConfigFetchInterval.unref(); }; const cacheRequest = async (request: RequestType, baseUrl: string) => { @@ -248,7 +247,7 @@ const Supergood = () => { // Force flush cache means don't wait for responses const flushCache = async ({ force } = { force: false }) => { - log.debug('Flushing Cache ...', { force }); + // log.debug('Flushing Cache ...', { force }); const responseCacheKeys = responseCache.keys(); const requestCacheKeys = requestCache.keys(); @@ -269,7 +268,7 @@ const Supergood = () => { } if (data.length === 0) { - log.debug('Nothing to flush', { force }); + // log.debug('Nothing to flush', { force }); return; } @@ -279,13 +278,16 @@ const Supergood = () => { } else { await postEvents(eventSinkUrl, data, headerOptions); } - log.debug(`Flushed ${data.length} events`, { force }); + if (data.length) { + log.debug(`Flushed ${data.length} events`, { force }); + log.debug(`Flushing Ids: ${data.map((event) => event.request.id)}`) + } } catch (e) { const error = e as Error; if (error.message === errors.UNAUTHORIZED) { log.error( errors.UNAUTHORIZED, - { config: supergoodConfig, metadata: { ...supergoodMetadata }}, + { config: supergoodConfig, metadata: { ...supergoodMetadata } }, error, { reportOut: false diff --git a/src/utils.ts b/src/utils.ts index 0bad0d0..5d7f8f2 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -195,7 +195,7 @@ const redactValue = ( dataType = 'array'; } else if (typeof input === 'object') { - dataLength = new Blob([input.toString()]).size; + dataLength = new Blob([JSON.stringify(input)]).size; dataType = 'object'; } else if (typeof input === 'string') { dataLength = input.length; From 4d70d23a2b17c9821e904e574169522fb93b0fd0 Mon Sep 17 00:00:00 2001 From: Alex Klarfeld Date: Tue, 12 Dec 2023 15:55:00 -0800 Subject: [PATCH 22/30] Merged with master --- src/index.ts | 175 +++++++++++++++++++++++++++------------------------ src/utils.ts | 20 +++--- 2 files changed, 102 insertions(+), 93 deletions(-) diff --git a/src/index.ts b/src/index.ts index cfcf18c..6535793 100644 --- a/src/index.ts +++ b/src/index.ts @@ -124,95 +124,104 @@ const Supergood = () => { }; const initializeInterceptors = () => { - interceptor.setup(); - - // Continue fetching the remote config every milliseconds - remoteConfigFetchInterval = setInterval(fetchAndProcessRemoteConfig, supergoodConfig.remoteConfigFetchInterval); - - interceptor.on( - 'request', - async (request: IsomorphicRequest, requestId: string) => { - try { - const url = new URL(request.url); - // Meant for debug and testing purposes - - if (url.pathname === TestErrorPath) { - throw new Error(errors.TEST_ERROR); - } + interceptor.setup(); + interceptor.on( + 'request', + async (request: IsomorphicRequest, requestId: string) => { + // Don't intercept if there's no remote config set + // to avoid sensitive keys being sent to the SG server. + if(!supergoodConfig.remoteConfig) return; + + try { + const url = new URL(request.url); + // Meant for debug and testing purposes + if (url.pathname === TestErrorPath) { + throw new Error(errors.TEST_ERROR); + } - const body = await request.clone().text(); - const requestData = { - id: requestId, - headers: Object.fromEntries(request.headers.entries()), - method: request.method, - url: url.href, - path: url.pathname, - search: url.search, - body: safeParseJson(body), - requestedAt: new Date() - } as RequestType; - - cacheRequest(requestData, baseUrl); - } catch (e) { - log.error( - errors.CACHING_REQUEST, - { - config: supergoodConfig, - metadata: { - requestUrl: request.url.toString(), - payloadSize: serialize(request).length, - ...supergoodMetadata + const body = await request.clone().text(); + const requestData = { + id: requestId, + headers: Object.fromEntries(request.headers.entries()), + method: request.method, + url: url.href, + path: url.pathname, + search: url.search, + body: safeParseJson(body), + requestedAt: new Date() + } as RequestType; + + const endpointConfig = getEndpointConfigForRequest(requestData, supergoodConfig.remoteConfig); + if (endpointConfig?.ignored) return; + + cacheRequest(requestData, baseUrl); + } catch (e) { + log.error( + errors.CACHING_REQUEST, + { + config: supergoodConfig, + metadata: { + requestUrl: request.url.toString(), + payloadSize: serialize(request).length, + ...supergoodMetadata + } + }, + e as Error, + { + reportOut: !localOnly } - }, - e as Error, - { - reportOut: !localOnly - } - ); + ); + } } - } - ); - - interceptor.on( - 'response', - async (response: IsomorphicResponse, requestId: string) => { - let requestData = { url: '' }; - let responseData = {}; - - try { - const requestData = requestCache.get(requestId) as { - request: RequestType; - }; - - if (requestData) { - const responseData = { - response: { - headers: Object.fromEntries(response.headers.entries()), - status: response.status, - statusText: response.statusText, - body: response.body && safeParseJson(response.body), - respondedAt: new Date() + ); + + interceptor.on( + 'response', + async (response: IsomorphicResponse, requestId: string) => { + let requestData = { url: '' }; + let responseData = {}; + + if(!supergoodConfig.remoteConfig) return; + + try { + const requestData = requestCache.get(requestId) as { + request: RequestType; + }; + + if (requestData) { + + const endpointConfig = getEndpointConfigForRequest(requestData.request, supergoodConfig.remoteConfig); + if (endpointConfig?.ignored) return; + + const responseData = { + response: { + headers: Object.fromEntries(response.headers.entries()), + status: response.status, + statusText: response.statusText, + body: response.body && safeParseJson(response.body), + respondedAt: new Date() + }, + ...requestData + } as EventRequestType; + cacheResponse(responseData, baseUrl); + } + } catch (e) { + log.error( + errors.CACHING_RESPONSE, + { + config: supergoodConfig, + metadata: { + ...supergoodMetadata, + requestUrl: requestData.url, + payloadSize: responseData ? serialize(responseData).length : 0 + } }, - ...requestData - } as EventRequestType; - cacheResponse(responseData, baseUrl); + e as Error + ); } - } catch (e) { - log.error( - errors.CACHING_RESPONSE, - { - config: supergoodConfig, - metadata: { - ...supergoodMetadata, - requestUrl: requestData.url, - payloadSize: responseData ? serialize(responseData).length : 0 - } - }, - e as Error - ); } - } - ); + ); + }; // Fetch the initial config and process it await fetchAndProcessRemoteConfig(); diff --git a/src/utils.ts b/src/utils.ts index 5d7f8f2..0603222 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -87,18 +87,18 @@ const getHeaderOptions = ( }; const marshalKeyPath = (keypath: string) => { - if(/^request_headers/.test(keypath)) return keypath.replace('request_headers', 'request.headers'); - if(/^request_body/.test(keypath)) return keypath.replace('request_body', 'request.body'); - if(/^response_headers/.test(keypath)) return keypath.replace('response_headers', 'response.headers'); - if(/^response_body/.test(keypath)) return keypath.replace('response_body', 'response.body'); + if(/^requestHeaders/.test(keypath)) return keypath.replace('requestHeaders', 'request.headers'); + if(/^requestBody/.test(keypath)) return keypath.replace('requestBody', 'request.body'); + if(/^responseHeaders/.test(keypath)) return keypath.replace('responseHeaders', 'response.headers'); + if(/^responseBody/.test(keypath)) return keypath.replace('responseBody', 'response.body'); return keypath; } const unmarshalKeyPath = (keypath: string) => { - if(/^request\.headers/.test(keypath)) return keypath.replace('request.headers', 'request_headers'); - if(/^request\.body/.test(keypath)) return keypath.replace('request.body', 'request_body'); - if(/^response\.headers/.test(keypath)) return keypath.replace('response.headers', 'response_headers'); - if(/^response\.body/.test(keypath)) return keypath.replace('response.body', 'response_body'); + if(/^request\.headers/.test(keypath)) return keypath.replace('request.headers', 'requestHeaders'); + if(/^request\.body/.test(keypath)) return keypath.replace('request.body', 'requestBody'); + if(/^response\.headers/.test(keypath)) return keypath.replace('response.headers', 'responseHeaders'); + if(/^response\.body/.test(keypath)) return keypath.replace('response.body', 'responseBody'); return keypath; } @@ -357,8 +357,8 @@ const getStrRepresentationFromPath = (request: RequestType, location: string) => if(location === 'domain') return url.hostname.toString(); if(location === 'url') return url.toString(); if(location === 'path') return url.pathname.toString(); - if(location === 'request_headers') return request.headers.toString(); - if(location === 'request_body') return request.body?.toString(); + if(location === 'requestHeaders') return request.headers.toString(); + if(location === 'requestBody') return request.body?.toString(); return request[location as keyof RequestType]?.toString(); } From 40088630584cb63668d82cfcbf86bcac65f07403 Mon Sep 17 00:00:00 2001 From: Alex Klarfeld Date: Wed, 13 Dec 2023 09:11:04 -0800 Subject: [PATCH 23/30] Remove optional arguments --- src/interceptor/NodeClientRequest.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/interceptor/NodeClientRequest.ts b/src/interceptor/NodeClientRequest.ts index 6869632..935c36d 100644 --- a/src/interceptor/NodeClientRequest.ts +++ b/src/interceptor/NodeClientRequest.ts @@ -18,9 +18,6 @@ export type NodeClientOptions = { allowLocalUrls: boolean; baseUrl?: string; ignoredDomains?: string[]; - allowLocalUrls: boolean; - baseUrl?: string; - ignoredDomains?: string[]; }; export type Protocol = 'http' | 'https'; From 0a1085578c2cf279388c57f339fed001b8b2e710 Mon Sep 17 00:00:00 2001 From: Alex Klarfeld Date: Wed, 13 Dec 2023 09:58:52 -0800 Subject: [PATCH 24/30] Fixed camel case format --- src/utils.test.ts | 10 +++++----- src/utils.ts | 30 ++++++++++++++++-------------- 2 files changed, 21 insertions(+), 19 deletions(-) diff --git a/src/utils.test.ts b/src/utils.test.ts index bb5ce2f..596ff78 100644 --- a/src/utils.test.ts +++ b/src/utils.test.ts @@ -185,7 +185,7 @@ it('redacts values from keys with proper marshalling', () => { location: 'path', regex: '/posts', ignored: false, - sensitiveKeys: ['request_body.posts[].title'] + sensitiveKeys: ['requestBody.posts[].title'] } } }; @@ -193,7 +193,7 @@ it('redacts values from keys with proper marshalling', () => { const redactedObj = redactValuesFromKeys(obj, remoteConfig); expect(_get(redactedObj, 'event.request.body.posts[0].title')).toBeNull(); expect(redactedObj.sensitiveKeyMetadata[0]).toEqual({ - keyPath: "request_body.posts[0].title", + keyPath: "requestBody.posts[0].title", type: "string", length: 11, }) @@ -302,7 +302,7 @@ it('redacts values from keys of nested array', () => { location: 'path', regex: '/posts', ignored: false, - sensitiveKeys: ['request_body.posts[].comments[].body'] + sensitiveKeys: ['requestBody.posts[].comments[].body'] } } }; @@ -310,7 +310,7 @@ it('redacts values from keys of nested array', () => { const redactedObj = redactValuesFromKeys(obj, remoteConfig); expect(_get(redactedObj, 'event.request.body.posts[0].comments[0].body')).toBeNull(); expect(redactedObj.sensitiveKeyMetadata[0]).toEqual({ - keyPath: "request_body.posts[0].comments[0].body", + keyPath: "requestBody.posts[0].comments[0].body", type: "string", length: 12, }) @@ -386,7 +386,7 @@ it('will prepare the data appropriately for posting to the server', () => { location: 'path', regex: '/posts', ignored: false, - sensitiveKeys: ['response_body.comments'] + sensitiveKeys: ['responseBody.comments'] } } }; diff --git a/src/utils.ts b/src/utils.ts index 0603222..0e19ace 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -87,22 +87,23 @@ const getHeaderOptions = ( }; const marshalKeyPath = (keypath: string) => { - if(/^requestHeaders/.test(keypath)) return keypath.replace('requestHeaders', 'request.headers'); - if(/^requestBody/.test(keypath)) return keypath.replace('requestBody', 'request.body'); - if(/^responseHeaders/.test(keypath)) return keypath.replace('responseHeaders', 'response.headers'); - if(/^responseBody/.test(keypath)) return keypath.replace('responseBody', 'response.body'); + if (/^requestHeaders/.test(keypath)) return keypath.replace('requestHeaders', 'request.headers'); + if (/^requestBody/.test(keypath)) return keypath.replace('requestBody', 'request.body'); + if (/^responseHeaders/.test(keypath)) return keypath.replace('responseHeaders', 'response.headers'); + if (/^responseBody/.test(keypath)) return keypath.replace('responseBody', 'response.body'); return keypath; } const unmarshalKeyPath = (keypath: string) => { - if(/^request\.headers/.test(keypath)) return keypath.replace('request.headers', 'requestHeaders'); - if(/^request\.body/.test(keypath)) return keypath.replace('request.body', 'requestBody'); - if(/^response\.headers/.test(keypath)) return keypath.replace('response.headers', 'responseHeaders'); - if(/^response\.body/.test(keypath)) return keypath.replace('response.body', 'responseBody'); + if (/^request\.headers/.test(keypath)) return keypath.replace('request.headers', 'requestHeaders'); + if (/^request\.body/.test(keypath)) return keypath.replace('request.body', 'requestBody'); + if (/^response\.headers/.test(keypath)) return keypath.replace('response.headers', 'responseHeaders'); + if (/^response\.body/.test(keypath)) return keypath.replace('response.body', 'responseBody'); return keypath; } const expandSensitiveKeySetForArrays = (obj: any, sensitiveKeys: Array): Array => { + console.log({ sensitiveKeys }) const expandKey = (key: string, obj: any): Array => { // Split the key by dots, considering the array brackets as part of the key const parts = key.match(/[^.\[\]]+|\[\d*\]|\[\*\]/g) || []; @@ -156,6 +157,7 @@ const redactValuesFromKeys = ( ): { event: { request?: RequestType; response?: ResponseType }, sensitiveKeyMetadata: Array } => { let sensitiveKeyMetadata: Array = []; const endpointConfig = getEndpointConfigForRequest(event.request as RequestType, remoteConfig); + console.log({ endpointConfig }) if (!endpointConfig || !endpointConfig?.sensitiveKeys?.length) return { event, sensitiveKeyMetadata }; else { const sensitiveKeys = expandSensitiveKeySetForArrays(event, endpointConfig.sensitiveKeys.map(key => marshalKeyPath(key))) @@ -186,7 +188,7 @@ const redactValue = ( let dataLength; let dataType; - if(!input) { + if (!input) { dataLength = 0; dataType = 'null'; } @@ -354,11 +356,11 @@ const sleep = (ms: number) => { const getStrRepresentationFromPath = (request: RequestType, location: string) => { const url = new URL(request.url); - if(location === 'domain') return url.hostname.toString(); - if(location === 'url') return url.toString(); - if(location === 'path') return url.pathname.toString(); - if(location === 'requestHeaders') return request.headers.toString(); - if(location === 'requestBody') return request.body?.toString(); + if (location === 'domain') return url.hostname.toString(); + if (location === 'url') return url.toString(); + if (location === 'path') return url.pathname.toString(); + if (location === 'requestHeaders') return request.headers.toString(); + if (location === 'requestBody') return request.body?.toString(); return request[location as keyof RequestType]?.toString(); } From 7ba3101b07c3a791b69cde8a78c96ec52e9b85e4 Mon Sep 17 00:00:00 2001 From: Alex Klarfeld Date: Wed, 13 Dec 2023 11:01:23 -0800 Subject: [PATCH 25/30] Added remote config fetching --- src/utils.test.ts | 17 ++++++++++++---- src/utils.ts | 2 -- yarn.lock | 50 ----------------------------------------------- 3 files changed, 13 insertions(+), 56 deletions(-) diff --git a/src/utils.test.ts b/src/utils.test.ts index 596ff78..3f20074 100644 --- a/src/utils.test.ts +++ b/src/utils.test.ts @@ -364,7 +364,11 @@ it('will prepare the data appropriately for posting to the server', () => { path: '/posts', search: '', requestedAt: new Date(), - body: {}, + body: { + blogType: { + name: 'My Blog', + } + }, }, response: { headers: {}, @@ -373,6 +377,10 @@ it('will prepare the data appropriately for posting to the server', () => { respondedAt: new Date(), body: { name: 'My Blog', + user: { + name: 'John Doe', + email: 'john@doe.com', + }, comments: [ 1,2,3,4 ] @@ -386,12 +394,13 @@ it('will prepare the data appropriately for posting to the server', () => { location: 'path', regex: '/posts', ignored: false, - sensitiveKeys: ['responseBody.comments'] + sensitiveKeys: ['responseBody.user.email', 'requestBody.blogType.name'] } } }; const events = prepareData([obj], remoteConfig); - expect(_get(events[0], 'response.body.name')).toBeTruthy(); - expect(events[0].metadata.sensitiveKeys.length).toEqual(1) + expect(_get(events[0], 'response.body.user.email')).toBeFalsy(); + expect(_get(events[0], 'request.body.blogType.name')).toBeFalsy(); + expect(events[0].metadata.sensitiveKeys.length).toEqual(2) }); diff --git a/src/utils.ts b/src/utils.ts index 0e19ace..d53627e 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -103,7 +103,6 @@ const unmarshalKeyPath = (keypath: string) => { } const expandSensitiveKeySetForArrays = (obj: any, sensitiveKeys: Array): Array => { - console.log({ sensitiveKeys }) const expandKey = (key: string, obj: any): Array => { // Split the key by dots, considering the array brackets as part of the key const parts = key.match(/[^.\[\]]+|\[\d*\]|\[\*\]/g) || []; @@ -157,7 +156,6 @@ const redactValuesFromKeys = ( ): { event: { request?: RequestType; response?: ResponseType }, sensitiveKeyMetadata: Array } => { let sensitiveKeyMetadata: Array = []; const endpointConfig = getEndpointConfigForRequest(event.request as RequestType, remoteConfig); - console.log({ endpointConfig }) if (!endpointConfig || !endpointConfig?.sensitiveKeys?.length) return { event, sensitiveKeyMetadata }; else { const sensitiveKeys = expandSensitiveKeySetForArrays(event, endpointConfig.sensitiveKeys.map(key => marshalKeyPath(key))) diff --git a/yarn.lock b/yarn.lock index eb2b8b6..2dcd9c8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1470,16 +1470,6 @@ color-convert@^2.0.1: dependencies: color-name "~1.1.4" -color-name@1.1.3: - version "1.1.3" - resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" - integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw== - -color-name@~1.1.4: - version "1.1.4" - resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" - integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== - color-name@1.1.3: version "1.1.3" resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz" @@ -2243,11 +2233,6 @@ function-bind@^1.1.1, function-bind@^1.1.2: resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz" integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== -function-bind@^1.1.2: - version "1.1.2" - resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz" - integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== - gensync@^1.0.0-beta.2: version "1.0.0-beta.2" resolved "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" @@ -2459,26 +2444,6 @@ headers-polyfill@^4.0.2: resolved "https://registry.npmjs.org/headers-polyfill/-/headers-polyfill-4.0.2.tgz" integrity sha512-EWGTfnTqAO2L/j5HZgoM/3z82L7necsJ0pO9Tp0X1wil3PDLrkypTBRgVO2ExehEEvUycejZD3FuRaXpZZc3kw== -help-me@^4.0.1: - version "4.2.0" - resolved "https://registry.npmjs.org/help-me/-/help-me-4.2.0.tgz#50712bfd799ff1854ae1d312c36eafcea85b0563" - integrity sha512-TAOnTB8Tz5Dw8penUuzHVrKNKlCIbwwbHnXraNJxPwf8LRtE2HlM84RYuezMFcwOJmoYOCWVDyJ8TQGxn9PgxA== - dependencies: - glob "^8.0.0" - readable-stream "^3.6.0" - -hasown@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz" - integrity sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA== - dependencies: - function-bind "^1.1.2" - -headers-polyfill@^4.0.2: - version "4.0.2" - resolved "https://registry.npmjs.org/headers-polyfill/-/headers-polyfill-4.0.2.tgz" - integrity sha512-EWGTfnTqAO2L/j5HZgoM/3z82L7necsJ0pO9Tp0X1wil3PDLrkypTBRgVO2ExehEEvUycejZD3FuRaXpZZc3kw== - hexoid@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/hexoid/-/hexoid-1.0.0.tgz" @@ -3869,16 +3834,6 @@ process@^0.11.10: resolved "https://registry.npmjs.org/process/-/process-0.11.10.tgz" integrity sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A== -process-warning@^2.0.0: - version "2.3.1" - resolved "https://registry.npmjs.org/process-warning/-/process-warning-2.3.1.tgz" - integrity sha512-JjBvFEn7MwFbzUDa2SRtKJSsyO0LlER4V/FmwLMhBlXNbGgGxdyFCxIdMDLerWUycsVUyaoM9QFLvppFy4IWaQ== - -process@^0.11.10: - version "0.11.10" - resolved "https://registry.npmjs.org/process/-/process-0.11.10.tgz" - integrity sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A== - prompts@^2.0.1: version "2.4.2" resolved "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz#7b57e73b3a48029ad10ebd44f74b01722a4cb069" @@ -4554,11 +4509,6 @@ url-parse-lax@^3.0.0: dependencies: prepend-http "^2.0.0" -util-deprecate@^1.0.1: - version "1.0.2" - resolved "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" - integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== - util@^0.12.3: version "0.12.5" resolved "https://registry.npmjs.org/util/-/util-0.12.5.tgz" From 539eedf46566ef4a66d0502b4cf24b8c198c51d7 Mon Sep 17 00:00:00 2001 From: Alex Klarfeld Date: Wed, 13 Dec 2023 11:15:46 -0800 Subject: [PATCH 26/30] add remote config --- package.json | 1 - yarn.lock | 116 --------------------------------------------------- 2 files changed, 117 deletions(-) diff --git a/package.json b/package.json index f9b6650..afda973 100644 --- a/package.json +++ b/package.json @@ -55,7 +55,6 @@ "@types/lodash.set": "^4.3.7", "@types/signal-exit": "^3.0.1", "@types/superagent": "^4.1.16", - "@typescript-eslint/eslint-plugin": "^6.13.2", "@typescript-eslint/parser": "^5.49.0", "axios": "^1.4.0", "dotenv": "^16.0.3", diff --git a/yarn.lock b/yarn.lock index 2dcd9c8..159f8c1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -297,18 +297,6 @@ resolved "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== -"@eslint-community/eslint-utils@^4.4.0": - version "4.4.0" - resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz#a23514e8fb9af1269d5f7788aa556798d61c6b59" - integrity sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA== - dependencies: - eslint-visitor-keys "^3.3.0" - -"@eslint-community/regexpp@^4.5.1": - version "4.10.0" - resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.10.0.tgz#548f6de556857c8bb73bbee70c35dc82a2e74d63" - integrity sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA== - "@eslint/eslintrc@^1.4.1": version "1.4.1" resolved "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.4.1.tgz" @@ -741,11 +729,6 @@ expect "^29.0.0" pretty-format "^29.0.0" -"@types/json-schema@^7.0.12": - version "7.0.15" - resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" - integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== - "@types/json-schema@^7.0.9": version "7.0.11" resolved "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz" @@ -826,11 +809,6 @@ resolved "https://registry.npmjs.org/@types/semver/-/semver-7.3.12.tgz" integrity sha512-WwA1MW0++RfXmCr12xeYOOC5baSC9mSb0ZqCquFzKhcoF4TvHu5MKOuXsncgZcpVFhB1pXd5hZmM0ryAoCp12A== -"@types/semver@^7.5.0": - version "7.5.6" - resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.6.tgz#c65b2bfce1bec346582c07724e3f8c1017a20339" - integrity sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A== - "@types/serve-static@*": version "1.15.0" resolved "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.0.tgz" @@ -869,23 +847,6 @@ dependencies: "@types/yargs-parser" "*" -"@typescript-eslint/eslint-plugin@^6.13.2": - version "6.13.2" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.13.2.tgz#2e03506c5362a65e43cb132c37c9ce2d3cb51470" - integrity sha512-3+9OGAWHhk4O1LlcwLBONbdXsAhLjyCFogJY/cWy2lxdVJ2JrcTF2pTGMaLl2AE7U1l31n8Py4a8bx5DLf/0dQ== - dependencies: - "@eslint-community/regexpp" "^4.5.1" - "@typescript-eslint/scope-manager" "6.13.2" - "@typescript-eslint/type-utils" "6.13.2" - "@typescript-eslint/utils" "6.13.2" - "@typescript-eslint/visitor-keys" "6.13.2" - debug "^4.3.4" - graphemer "^1.4.0" - ignore "^5.2.4" - natural-compare "^1.4.0" - semver "^7.5.4" - ts-api-utils "^1.0.1" - "@typescript-eslint/parser@^5.49.0": version "5.49.0" resolved "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.49.0.tgz" @@ -904,34 +865,11 @@ "@typescript-eslint/types" "5.49.0" "@typescript-eslint/visitor-keys" "5.49.0" -"@typescript-eslint/scope-manager@6.13.2": - version "6.13.2" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-6.13.2.tgz#5fa4e4adace028dafac212c770640b94e7b61052" - integrity sha512-CXQA0xo7z6x13FeDYCgBkjWzNqzBn8RXaE3QVQVIUm74fWJLkJkaHmHdKStrxQllGh6Q4eUGyNpMe0b1hMkXFA== - dependencies: - "@typescript-eslint/types" "6.13.2" - "@typescript-eslint/visitor-keys" "6.13.2" - -"@typescript-eslint/type-utils@6.13.2": - version "6.13.2" - resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-6.13.2.tgz#ebec2da14a6bb7122e0fd31eea72a382c39c6102" - integrity sha512-Qr6ssS1GFongzH2qfnWKkAQmMUyZSyOr0W54nZNU1MDfo+U4Mv3XveeLZzadc/yq8iYhQZHYT+eoXJqnACM1tw== - dependencies: - "@typescript-eslint/typescript-estree" "6.13.2" - "@typescript-eslint/utils" "6.13.2" - debug "^4.3.4" - ts-api-utils "^1.0.1" - "@typescript-eslint/types@5.49.0": version "5.49.0" resolved "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.49.0.tgz" integrity sha512-7If46kusG+sSnEpu0yOz2xFv5nRz158nzEXnJFCGVEHWnuzolXKwrH5Bsf9zsNlOQkyZuk0BZKKoJQI+1JPBBg== -"@typescript-eslint/types@6.13.2": - version "6.13.2" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-6.13.2.tgz#c044aac24c2f6cefb8e921e397acad5417dd0ae6" - integrity sha512-7sxbQ+EMRubQc3wTfTsycgYpSujyVbI1xw+3UMRUcrhSy+pN09y/lWzeKDbvhoqcRbHdc+APLs/PWYi/cisLPg== - "@typescript-eslint/typescript-estree@5.49.0": version "5.49.0" resolved "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.49.0.tgz" @@ -945,32 +883,6 @@ semver "^7.3.7" tsutils "^3.21.0" -"@typescript-eslint/typescript-estree@6.13.2": - version "6.13.2" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-6.13.2.tgz#ae556ee154c1acf025b48d37c3ef95a1d55da258" - integrity sha512-SuD8YLQv6WHnOEtKv8D6HZUzOub855cfPnPMKvdM/Bh1plv1f7Q/0iFUDLKKlxHcEstQnaUU4QZskgQq74t+3w== - dependencies: - "@typescript-eslint/types" "6.13.2" - "@typescript-eslint/visitor-keys" "6.13.2" - debug "^4.3.4" - globby "^11.1.0" - is-glob "^4.0.3" - semver "^7.5.4" - ts-api-utils "^1.0.1" - -"@typescript-eslint/utils@6.13.2": - version "6.13.2" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-6.13.2.tgz#8eb89e53adc6d703a879b131e528807245486f89" - integrity sha512-b9Ptq4eAZUym4idijCRzl61oPCwwREcfDI8xGk751Vhzig5fFZR9CyzDz4Sp/nxSLBYxUPyh4QdIDqWykFhNmQ== - dependencies: - "@eslint-community/eslint-utils" "^4.4.0" - "@types/json-schema" "^7.0.12" - "@types/semver" "^7.5.0" - "@typescript-eslint/scope-manager" "6.13.2" - "@typescript-eslint/types" "6.13.2" - "@typescript-eslint/typescript-estree" "6.13.2" - semver "^7.5.4" - "@typescript-eslint/utils@^5.10.0": version "5.49.0" resolved "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.49.0.tgz" @@ -993,14 +905,6 @@ "@typescript-eslint/types" "5.49.0" eslint-visitor-keys "^3.3.0" -"@typescript-eslint/visitor-keys@6.13.2": - version "6.13.2" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-6.13.2.tgz#e0a4a80cf842bb08e6127b903284166ac4a5594c" - integrity sha512-OGznFs0eAQXJsp+xSd6k/O1UbFi/K/L7WjqeRoFE7vadjAF9y0uppXhYNQNEqygjou782maGClOoZwPqF0Drlw== - dependencies: - "@typescript-eslint/types" "6.13.2" - eslint-visitor-keys "^3.4.1" - "@zxing/text-encoding@0.9.0": version "0.9.0" resolved "https://registry.npmjs.org/@zxing/text-encoding/-/text-encoding-0.9.0.tgz" @@ -1869,11 +1773,6 @@ eslint-visitor-keys@^3.3.0: resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz" integrity sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA== -eslint-visitor-keys@^3.4.1: - version "3.4.3" - resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800" - integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag== - eslint@^8.32.0: version "8.33.0" resolved "https://registry.npmjs.org/eslint/-/eslint-8.33.0.tgz" @@ -2383,11 +2282,6 @@ grapheme-splitter@^1.0.4: resolved "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz" integrity sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ== -graphemer@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6" - integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag== - has-flag@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" @@ -2499,11 +2393,6 @@ ignore@^5.2.0: resolved "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz" integrity sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ== -ignore@^5.2.4: - version "5.3.0" - resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.0.tgz#67418ae40d34d6999c95ff56016759c718c82f78" - integrity sha512-g7dmpshy+gD7mh88OC9NwSGTKoc3kyLAZQRU1mt53Aw/vnvfXnbC+F/7F7QoYVKbV+KNvJx8wArewKy1vXMtlg== - import-fresh@^3.0.0, import-fresh@^3.2.1: version "3.3.0" resolved "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz" @@ -4365,11 +4254,6 @@ tr46@~0.0.3: resolved "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz" integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== -ts-api-utils@^1.0.1: - version "1.0.3" - resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-1.0.3.tgz#f12c1c781d04427313dbac808f453f050e54a331" - integrity sha512-wNMeqtMz5NtwpT/UZGY5alT+VoKdSsOOP/kqHFcUW1P/VRhH2wJ48+DN2WwUliNbQ976ETwDL0Ifd2VVvgonvg== - ts-essentials@^9.4.1: version "9.4.1" resolved "https://registry.npmjs.org/ts-essentials/-/ts-essentials-9.4.1.tgz" From 2530cb2f4faff62f559c87bb100892b4c670bee0 Mon Sep 17 00:00:00 2001 From: Alex Klarfeld Date: Wed, 13 Dec 2023 11:35:53 -0800 Subject: [PATCH 27/30] Fixed tests --- test/e2e/remote-config.e2e.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/e2e/remote-config.e2e.test.ts b/test/e2e/remote-config.e2e.test.ts index 42456f1..73899dd 100644 --- a/test/e2e/remote-config.e2e.test.ts +++ b/test/e2e/remote-config.e2e.test.ts @@ -75,7 +75,7 @@ describe('remote config functionality', () => { endpointConfiguration: { action: 'Allow', sensitiveKeys: [{ - keyPath: 'response_body.name' + keyPath: 'responseBody.name' }] } }] From d484bcab19aeba2599a1fd907a1a1fdd4d08d451 Mon Sep 17 00:00:00 2001 From: Alex Klarfeld Date: Mon, 18 Dec 2023 09:18:29 -0800 Subject: [PATCH 28/30] Fixed remote config --- src/index.ts | 15 +++++++-------- src/utils.ts | 1 + 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/index.ts b/src/index.ts index 6535793..705e74d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -65,11 +65,11 @@ const Supergood = () => { config?: Partial; metadata?: Partial; } = { - clientId: process.env.SUPERGOOD_CLIENT_ID as string, - clientSecret: process.env.SUPERGOOD_CLIENT_SECRET as string, - config: {} as Partial, - metadata: {} as Partial - }, + clientId: process.env.SUPERGOOD_CLIENT_ID as string, + clientSecret: process.env.SUPERGOOD_CLIENT_SECRET as string, + config: {} as Partial, + metadata: {} as Partial + }, baseUrl = process.env.SUPERGOOD_BASE_URL || 'https://api.supergood.ai' ) => { if (!clientId) throw new Error(errors.NO_CLIENT_ID); @@ -130,7 +130,7 @@ const Supergood = () => { async (request: IsomorphicRequest, requestId: string) => { // Don't intercept if there's no remote config set // to avoid sensitive keys being sent to the SG server. - if(!supergoodConfig.remoteConfig) return; + if (!supergoodConfig.remoteConfig) return; try { const url = new URL(request.url); @@ -181,7 +181,7 @@ const Supergood = () => { let requestData = { url: '' }; let responseData = {}; - if(!supergoodConfig.remoteConfig) return; + if (!supergoodConfig.remoteConfig) return; try { const requestData = requestCache.get(requestId) as { @@ -289,7 +289,6 @@ const Supergood = () => { } if (data.length) { log.debug(`Flushed ${data.length} events`, { force }); - log.debug(`Flushing Ids: ${data.map((event) => event.request.id)}`) } } catch (e) { const error = e as Error; diff --git a/src/utils.ts b/src/utils.ts index d53627e..a6e0e79 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -165,6 +165,7 @@ const redactValuesFromKeys = ( const value = _get(event, keyPath); if (value) { _set(event, keyPath, null); + // Don't return : for null values sensitiveKeyMetadata.push({ keyPath: unmarshalKeyPath(keyPath), ...redactValue(value) }); } } From 9ca33263dd20be59e00ef1b4c75f95c7fbe7f0a1 Mon Sep 17 00:00:00 2001 From: Pavlo <5280742+kurpav@users.noreply.github.com> Date: Mon, 18 Dec 2023 19:16:25 +0200 Subject: [PATCH 29/30] test: refactor tests (#22) * test: rewrite core tests to use expect logic only * test: update libraries tests * test: udpate tests descriptions and do minor refactoring * test: refactor results checks * test: increase timeouts for tests with external api --- test/e2e/axios.e2e.test.ts | 20 +-- test/e2e/core.e2e.test.ts | 222 +++++++++++++----------------- test/e2e/native-fetch.e2e.test.ts | 22 +-- test/e2e/node-fetch.e2e.test.ts | 22 +-- test/e2e/superagent.e2e.test.ts | 21 +-- test/e2e/undici.e2e.test.ts | 25 ++-- test/utils/function-call-args.ts | 17 +++ 7 files changed, 178 insertions(+), 171 deletions(-) diff --git a/test/e2e/axios.e2e.test.ts b/test/e2e/axios.e2e.test.ts index 82cac1c..653b818 100644 --- a/test/e2e/axios.e2e.test.ts +++ b/test/e2e/axios.e2e.test.ts @@ -7,8 +7,8 @@ import { SUPERGOOD_CLIENT_SECRET, SUPERGOOD_SERVER } from '../consts'; -import { getEvents } from '../utils/function-call-args'; import { mockApi } from '../utils/mock-api'; +import { checkPostedEvents } from '../utils/function-call-args'; describe('axios library', () => { const { postEventsMock } = mockApi(); @@ -30,10 +30,12 @@ describe('axios library', () => { const response = await axios.get(`${MOCK_DATA_SERVER}/posts`); expect(response.status).toEqual(200); await Supergood.close(); - const eventsPosted = getEvents(postEventsMock); - expect(eventsPosted.length).toEqual(1); - expect(eventsPosted[0].response.body).toEqual(response.data); + checkPostedEvents(postEventsMock, 1, { + response: expect.objectContaining({ + body: response.data + }) + }); }); it('POST /posts', async () => { @@ -45,9 +47,11 @@ describe('axios library', () => { expect(response.status).toEqual(201); await Supergood.close(); - const eventsPosted = getEvents(postEventsMock); - expect(eventsPosted[0].request.body).toEqual(body); - expect(eventsPosted[0].response.body).toEqual(response.data); - expect(eventsPosted.length).toEqual(1); + checkPostedEvents(postEventsMock, 1, { + response: expect.objectContaining({ + body: response.data + }), + request: expect.objectContaining({ body }) + }); }); }); diff --git a/test/e2e/core.e2e.test.ts b/test/e2e/core.e2e.test.ts index 7eeb272..e1405e4 100644 --- a/test/e2e/core.e2e.test.ts +++ b/test/e2e/core.e2e.test.ts @@ -1,11 +1,8 @@ -import get from 'lodash.get'; - import axios from 'axios'; import fetch from 'node-fetch'; import Supergood from '../../src'; import { LocalClientId, LocalClientSecret, errors } from '../../src/constants'; -import { EventRequestType } from '../../src/types'; import { sleep } from '../../src/utils'; import { @@ -16,14 +13,14 @@ import { SUPERGOOD_CONFIG, SUPERGOOD_SERVER } from '../consts'; -import { getErrors, getEvents } from '../utils/function-call-args'; import { mockApi } from '../utils/mock-api'; +import { checkPostedEvents } from '../utils/function-call-args'; describe('core functionality', () => { const { postEventsMock, postErrorMock } = mockApi(); - describe('testing success states', () => { - test('captures all outgoing 200 http requests', async () => { + describe('success states', () => { + it('should capture all outgoing 200 http requests', async () => { await Supergood.init( { config: { ...SUPERGOOD_CONFIG, allowLocalUrls: true }, @@ -37,30 +34,19 @@ describe('core functionality', () => { await axios.get(`${MOCK_DATA_SERVER}/posts`); } await Supergood.close(); - // checking that all events were posted - expect(postEventsMock).toHaveBeenCalledWith( - expect.any(String), - expect.toBeArrayOfSize(numberOfHttpCalls), - expect.any(Object) - ); - expect(postEventsMock).toHaveBeenCalledWith( - expect.any(String), - expect.arrayContaining([ - expect.objectContaining({ - request: expect.objectContaining({ - requestedAt: expect.any(Date) - }), - response: expect.objectContaining({ - respondedAt: expect.any(Date) - }) - }) - ]), - expect.any(Object) - ); + + checkPostedEvents(postEventsMock, numberOfHttpCalls, { + request: expect.objectContaining({ + requestedAt: expect.any(Date) + }), + response: expect.objectContaining({ + respondedAt: expect.any(Date) + }) + }); }); - test('captures non-success status and errors', async () => { - const httpErrorCodes = [400, 401, 403, 404, 500, 501, 502, 503, 504]; + it('should capture non-success statuses and errors', async () => { + const HTTP_ERROR_CODES = [400, 401, 403, 404, 500, 501, 502, 503, 504]; await Supergood.init( { config: { ...SUPERGOOD_CONFIG, allowLocalUrls: true }, @@ -69,7 +55,7 @@ describe('core functionality', () => { }, SUPERGOOD_SERVER ); - for (const code of httpErrorCodes) { + for (const code of HTTP_ERROR_CODES) { try { await axios.get(`${MOCK_DATA_SERVER}/${code}`); } catch (e) { @@ -78,28 +64,16 @@ describe('core functionality', () => { } await Supergood.close(); - // checking that all events were posted - expect(postEventsMock).toHaveBeenCalledWith( - expect.any(String), - expect.toBeArrayOfSize(httpErrorCodes.length), - expect.any(Object) - ); - expect(postEventsMock).toHaveBeenCalledWith( - expect.any(String), - expect.arrayContaining([ - expect.objectContaining({ - response: expect.objectContaining({ - status: expect.toBeOneOf(httpErrorCodes) - }) - }) - ]), - expect.any(Object) - ); + checkPostedEvents(postEventsMock, HTTP_ERROR_CODES.length, { + response: expect.objectContaining({ + status: expect.toBeOneOf(HTTP_ERROR_CODES) + }) + }); }); }); - describe('testing failure states', () => { - test('hanging response', async () => { + describe('failure states', () => { + it('hanging response', async () => { await Supergood.init( { config: { ...SUPERGOOD_CONFIG, allowLocalUrls: true }, @@ -112,27 +86,14 @@ describe('core functionality', () => { await sleep(1000); await Supergood.close(); - // checking that all events were posted - expect(postEventsMock).toHaveBeenCalledWith( - expect.any(String), - expect.toBeArrayOfSize(1), - expect.any(Object) - ); - expect(postEventsMock).toHaveBeenCalledWith( - expect.any(String), - expect.arrayContaining([ - expect.objectContaining({ - request: expect.objectContaining({ - requestedAt: expect.any(Date) - }) - }) - ]), - expect.any(Object) - ); + checkPostedEvents(postEventsMock, 1, { + request: expect.objectContaining({ + requestedAt: expect.any(Date) + }) + }); }, 10000); - // Causes the github actions to hang for some reason - test('posting errors', async () => { + it('posting errors', async () => { postEventsMock.mockImplementationOnce(() => { throw new Error(); }); @@ -146,9 +107,14 @@ describe('core functionality', () => { ); await axios.get(`${MOCK_DATA_SERVER}/posts`); await Supergood.close(); - const postedErrors = getErrors(postErrorMock); expect(postErrorMock).toHaveBeenCalled(); - expect(postedErrors.message).toEqual(errors.POSTING_EVENTS); + expect(postErrorMock).toHaveBeenCalledWith( + expect.any(String), + expect.objectContaining({ + message: errors.POSTING_EVENTS + }), + expect.any(Object) + ); }); }); @@ -167,15 +133,15 @@ describe('core functionality', () => { ); await axios.get(`${MOCK_DATA_SERVER}/posts`); await Supergood.close(); - const eventsPosted = getEvents(postEventsMock); - const firstEvent = eventsPosted[0] as EventRequestType; - const hashedBodyValue = get(firstEvent, ['response', 'body', '0']); - expect( - hashedBodyValue && hashedBodyValue.match(BASE64_REGEX) - ).toBeTruthy(); + + checkPostedEvents(postEventsMock, 1, { + response: expect.objectContaining({ + body: expect.arrayContaining([expect.stringMatching(BASE64_REGEX)]) + }) + }); }); - test('not hashing', async () => { + it('should not hash anything', async () => { await Supergood.init( { config: { keysToHash: [], allowLocalUrls: true }, @@ -184,17 +150,17 @@ describe('core functionality', () => { }, SUPERGOOD_SERVER ); - await axios.get(`${MOCK_DATA_SERVER}/posts`); + const response = await axios.get(`${MOCK_DATA_SERVER}/posts`); await Supergood.close(); - const eventsPosted = getEvents(postEventsMock); - const firstEvent = eventsPosted[0] as EventRequestType; - expect( - typeof get(firstEvent, ['response', 'body']) === 'object' - ).toBeTruthy(); - expect(get(firstEvent, ['response', 'body', 'hashed'])).toBeFalsy(); + + checkPostedEvents(postEventsMock, 1, { + response: expect.objectContaining({ + body: response.data + }) + }); }); - test('keys to hash not in config', async () => { + it('should not hash if provided keys do not exist', async () => { await Supergood.init( { config: { @@ -206,17 +172,17 @@ describe('core functionality', () => { }, SUPERGOOD_SERVER ); - await axios.get(`${MOCK_DATA_SERVER}/posts`); + const response = await axios.get(`${MOCK_DATA_SERVER}/posts`); await Supergood.close(); - const eventsPosted = getEvents(postEventsMock); - const firstEvent = eventsPosted[0] as EventRequestType; - expect( - typeof get(firstEvent, ['response', 'body']) === 'object' - ).toBeTruthy(); - expect(get(firstEvent, ['response', 'body', 'hashed'])).toBeFalsy(); + + checkPostedEvents(postEventsMock, 1, { + response: expect.objectContaining({ + body: response.data + }) + }); }); - test('ignores requests to ignored domains', async () => { + it('should ignore requests to ignored domains', async () => { await Supergood.init( { config: { @@ -231,9 +197,9 @@ describe('core functionality', () => { await axios.get('https://supergood-testbed.herokuapp.com/200'); await Supergood.close(); expect(postEventsMock).not.toHaveBeenCalled(); - }); + }, 10000); - test('operates normally when ignored domains is empty', async () => { + it('should operate normally when ignored domains is empty', async () => { await Supergood.init( { config: { ignoredDomains: [], allowLocalUrls: true }, @@ -245,9 +211,9 @@ describe('core functionality', () => { await axios.get('https://supergood-testbed.herokuapp.com/200'); await Supergood.close(); expect(postEventsMock).toHaveBeenCalled(); - }); + }, 10000); - test('only posts for specified domains, ignores everything else', async () => { + it('should only post events for specified domains and ignore everything else', async () => { await Supergood.init( { config: { @@ -259,15 +225,21 @@ describe('core functionality', () => { }, SUPERGOOD_SERVER ); - await axios.get('https://api.ipify.org?format=json'); + const allowedUrl = new URL('https://api.ipify.org?format=json'); + await axios.get(allowedUrl.toString()); await axios.get('https://supergood-testbed.herokuapp.com/200'); await Supergood.close(); - expect(postEventsMock).toHaveBeenCalled(); - }); + + checkPostedEvents(postEventsMock, 1, { + request: expect.objectContaining({ + url: allowedUrl.toString() + }) + }); + }, 10000); }); - describe('non-standard payloads', () => { - test('gzipped response', async () => { + describe('encoding', () => { + it('should handle gzipped response', async () => { await Supergood.init( { clientId: SUPERGOOD_CLIENT_ID, @@ -279,21 +251,19 @@ describe('core functionality', () => { SUPERGOOD_SERVER ); const response = await fetch(`${MOCK_DATA_SERVER}/gzipped-response`); - const body = await response.text(); - await sleep(2000); - expect(response.status).toEqual(200); - expect(body).toBeTruthy(); + const responseBody = await response.json(); await Supergood.close(); - const eventsPosted = getEvents(postEventsMock); - expect(eventsPosted.length).toEqual(1); - expect(get(eventsPosted, '[0]response.body')).toEqual({ - gzippedResponse: 'this-is-gzipped' + + checkPostedEvents(postEventsMock, 1, { + response: expect.objectContaining({ + body: responseBody + }) }); }); }); - describe('captures headers', () => { - test('captures request headers', async () => { + describe('headers', () => { + it('should capture custom request headers', async () => { await Supergood.init( { clientId: SUPERGOOD_CLIENT_ID, @@ -314,14 +284,17 @@ describe('core functionality', () => { } }); await Supergood.close(); - const eventsPosted = getEvents(postEventsMock); - expect(eventsPosted.length).toEqual(1); - expect(get(eventsPosted, '[0]request.headers.x-custom-header')).toEqual( - 'custom-header-value' - ); + + checkPostedEvents(postEventsMock, 1, { + request: expect.objectContaining({ + headers: expect.objectContaining({ + 'x-custom-header': 'custom-header-value' + }) + }) + }); }); - test('capture response headers', async () => { + it('should capture custom response headers', async () => { await Supergood.init( { clientId: SUPERGOOD_CLIENT_ID, @@ -332,16 +305,19 @@ describe('core functionality', () => { ); await fetch(`${MOCK_DATA_SERVER}/custom-header`); await Supergood.close(); - const eventsPosted = getEvents(postEventsMock); - expect(eventsPosted.length).toEqual(1); - expect(get(eventsPosted, '[0]response.headers.x-custom-header')).toEqual( - 'custom-header-value' - ); + + checkPostedEvents(postEventsMock, 1, { + response: expect.objectContaining({ + headers: expect.objectContaining({ + 'x-custom-header': 'custom-header-value' + }) + }) + }); }); }); describe('local client id and secret', () => { - test('does not report out', async () => { + it('should not report out', async () => { await Supergood.init( { config: { ...SUPERGOOD_CONFIG, allowLocalUrls: true }, diff --git a/test/e2e/native-fetch.e2e.test.ts b/test/e2e/native-fetch.e2e.test.ts index e2dd305..60a969c 100644 --- a/test/e2e/native-fetch.e2e.test.ts +++ b/test/e2e/native-fetch.e2e.test.ts @@ -5,7 +5,7 @@ import { SUPERGOOD_CLIENT_SECRET, SUPERGOOD_SERVER } from '../consts'; -import { getEvents } from '../utils/function-call-args'; +import { checkPostedEvents } from '../utils/function-call-args'; import { mockApi } from '../utils/mock-api'; const [major] = process.versions.node.split('.').map(Number); @@ -31,10 +31,11 @@ describeIf('native fetch', () => { expect(response.status).toEqual(200); await Supergood.close(); - const eventsPosted = getEvents(postEventsMock); - - expect(eventsPosted.length).toEqual(1); - expect(eventsPosted[0].response.body).toEqual(responseBody); + checkPostedEvents(postEventsMock, 1, { + response: expect.objectContaining({ + body: responseBody + }) + }); }); it('POST /posts', async () => { @@ -50,10 +51,11 @@ describeIf('native fetch', () => { expect(response.status).toEqual(201); await Supergood.close(); - const eventsPosted = getEvents(postEventsMock); - - expect(eventsPosted[0].request.body).toEqual(body); - expect(eventsPosted[0].response.body).toEqual(responseBody); - expect(eventsPosted.length).toEqual(1); + checkPostedEvents(postEventsMock, 1, { + response: expect.objectContaining({ + body: responseBody + }), + request: expect.objectContaining({ body }) + }); }); }); diff --git a/test/e2e/node-fetch.e2e.test.ts b/test/e2e/node-fetch.e2e.test.ts index cd4adbb..3c94f91 100644 --- a/test/e2e/node-fetch.e2e.test.ts +++ b/test/e2e/node-fetch.e2e.test.ts @@ -7,7 +7,7 @@ import { SUPERGOOD_CLIENT_SECRET, SUPERGOOD_SERVER } from '../consts'; -import { getEvents } from '../utils/function-call-args'; +import { checkPostedEvents } from '../utils/function-call-args'; import { mockApi } from '../utils/mock-api'; describe('node-fetch library', () => { @@ -30,10 +30,11 @@ describe('node-fetch library', () => { expect(response.status).toEqual(200); await Supergood.close(); - const eventsPosted = getEvents(postEventsMock); - - expect(eventsPosted.length).toEqual(1); - expect(eventsPosted[0].response.body).toEqual(responseBody); + checkPostedEvents(postEventsMock, 1, { + response: expect.objectContaining({ + body: responseBody + }) + }); }); it('POST /posts', async () => { @@ -49,10 +50,11 @@ describe('node-fetch library', () => { expect(response.status).toEqual(201); await Supergood.close(); - const eventsPosted = getEvents(postEventsMock); - - expect(eventsPosted[0].request.body).toEqual(body); - expect(eventsPosted[0].response.body).toEqual(responseBody); - expect(eventsPosted.length).toEqual(1); + checkPostedEvents(postEventsMock, 1, { + response: expect.objectContaining({ + body: responseBody + }), + request: expect.objectContaining({ body }) + }); }); }); diff --git a/test/e2e/superagent.e2e.test.ts b/test/e2e/superagent.e2e.test.ts index f08835c..5b96d35 100644 --- a/test/e2e/superagent.e2e.test.ts +++ b/test/e2e/superagent.e2e.test.ts @@ -7,7 +7,7 @@ import { SUPERGOOD_CLIENT_SECRET, SUPERGOOD_SERVER } from '../consts'; -import { getEvents } from '../utils/function-call-args'; +import { checkPostedEvents } from '../utils/function-call-args'; import { mockApi } from '../utils/mock-api'; describe('superagent library', () => { @@ -28,10 +28,12 @@ describe('superagent library', () => { const response = await superagent.get(`${MOCK_DATA_SERVER}/posts`); expect(response.status).toEqual(200); await Supergood.close(); - const eventsPosted = getEvents(postEventsMock); - expect(eventsPosted.length).toEqual(1); - expect(eventsPosted[0].response.body).toEqual(response.body); + checkPostedEvents(postEventsMock, 1, { + response: expect.objectContaining({ + body: response.body + }) + }); }); it('POST /posts', async () => { @@ -45,11 +47,12 @@ describe('superagent library', () => { expect(response.status).toEqual(201); await Supergood.close(); - const eventsPosted = getEvents(postEventsMock); - // TODO: for some reason, the request body is empty - // expect(eventsPosted[0].request.body).toEqual(body); - expect(eventsPosted[0].response.body).toEqual(response.body); - expect(eventsPosted.length).toEqual(1); + checkPostedEvents(postEventsMock, 1, { + response: expect.objectContaining({ + body: response.body + }) + // request: expect.objectContaining({ body }) + }); }); }); diff --git a/test/e2e/undici.e2e.test.ts b/test/e2e/undici.e2e.test.ts index 43708c8..35e8c32 100644 --- a/test/e2e/undici.e2e.test.ts +++ b/test/e2e/undici.e2e.test.ts @@ -7,7 +7,7 @@ import { SUPERGOOD_CLIENT_SECRET, SUPERGOOD_SERVER } from '../consts'; -import { getEvents } from '../utils/function-call-args'; +import { checkPostedEvents } from '../utils/function-call-args'; import { mockApi } from '../utils/mock-api'; // TODO: post events mock is not being called @@ -31,16 +31,17 @@ describe.skip('undici library', () => { expect(response.statusCode).toEqual(200); await Supergood.close(); - expect(postEventsMock).toHaveBeenCalled(); - const eventsPosted = getEvents(postEventsMock); - expect(eventsPosted.length).toEqual(1); - expect(eventsPosted[0].response.body).toEqual(responseBody); + checkPostedEvents(postEventsMock, 1, { + response: expect.objectContaining({ + body: responseBody + }) + }); }); it('POST /posts', async () => { const body = { - title: 'axios-post', - author: 'axios-author' + title: 'undici-post', + author: 'undici-author' }; const response = await request(`${MOCK_DATA_SERVER}/posts`, { method: 'POST', @@ -50,9 +51,11 @@ describe.skip('undici library', () => { expect(response.statusCode).toEqual(201); await Supergood.close(); - const eventsPosted = getEvents(postEventsMock); - expect(eventsPosted[0].request.body).toEqual(body); - expect(eventsPosted[0].response.body).toEqual(responseBody); - expect(eventsPosted.length).toEqual(1); + checkPostedEvents(postEventsMock, 1, { + response: expect.objectContaining({ + body: responseBody + }), + request: expect.objectContaining({ body }) + }); }); }); diff --git a/test/utils/function-call-args.ts b/test/utils/function-call-args.ts index fb5f7bc..7d57f30 100644 --- a/test/utils/function-call-args.ts +++ b/test/utils/function-call-args.ts @@ -15,3 +15,20 @@ export const getErrors = ( mockedPostError.mock.calls.flat() )[1] as ErrorPayloadType; }; + +export function checkPostedEvents( + instance: jest.SpyInstance, + eventsCount: number, + eventContains: any +) { + expect(instance).toHaveBeenCalledWith( + expect.anything(), + expect.toBeArrayOfSize(eventsCount), + expect.any(Object) + ); + expect(instance).toHaveBeenCalledWith( + expect.anything(), + expect.arrayContaining([expect.objectContaining(eventContains)]), + expect.any(Object) + ); +} From bfaa97c380765992dead168397a95d15f600ced9 Mon Sep 17 00:00:00 2001 From: Alex Klarfeld Date: Thu, 7 Dec 2023 11:11:57 -0800 Subject: [PATCH 30/30] Added eslint config --- yarn.lock | 166 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 166 insertions(+) diff --git a/yarn.lock b/yarn.lock index 159f8c1..eb2b8b6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -297,6 +297,18 @@ resolved "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== +"@eslint-community/eslint-utils@^4.4.0": + version "4.4.0" + resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz#a23514e8fb9af1269d5f7788aa556798d61c6b59" + integrity sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA== + dependencies: + eslint-visitor-keys "^3.3.0" + +"@eslint-community/regexpp@^4.5.1": + version "4.10.0" + resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.10.0.tgz#548f6de556857c8bb73bbee70c35dc82a2e74d63" + integrity sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA== + "@eslint/eslintrc@^1.4.1": version "1.4.1" resolved "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.4.1.tgz" @@ -729,6 +741,11 @@ expect "^29.0.0" pretty-format "^29.0.0" +"@types/json-schema@^7.0.12": + version "7.0.15" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" + integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== + "@types/json-schema@^7.0.9": version "7.0.11" resolved "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz" @@ -809,6 +826,11 @@ resolved "https://registry.npmjs.org/@types/semver/-/semver-7.3.12.tgz" integrity sha512-WwA1MW0++RfXmCr12xeYOOC5baSC9mSb0ZqCquFzKhcoF4TvHu5MKOuXsncgZcpVFhB1pXd5hZmM0ryAoCp12A== +"@types/semver@^7.5.0": + version "7.5.6" + resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.6.tgz#c65b2bfce1bec346582c07724e3f8c1017a20339" + integrity sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A== + "@types/serve-static@*": version "1.15.0" resolved "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.0.tgz" @@ -847,6 +869,23 @@ dependencies: "@types/yargs-parser" "*" +"@typescript-eslint/eslint-plugin@^6.13.2": + version "6.13.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.13.2.tgz#2e03506c5362a65e43cb132c37c9ce2d3cb51470" + integrity sha512-3+9OGAWHhk4O1LlcwLBONbdXsAhLjyCFogJY/cWy2lxdVJ2JrcTF2pTGMaLl2AE7U1l31n8Py4a8bx5DLf/0dQ== + dependencies: + "@eslint-community/regexpp" "^4.5.1" + "@typescript-eslint/scope-manager" "6.13.2" + "@typescript-eslint/type-utils" "6.13.2" + "@typescript-eslint/utils" "6.13.2" + "@typescript-eslint/visitor-keys" "6.13.2" + debug "^4.3.4" + graphemer "^1.4.0" + ignore "^5.2.4" + natural-compare "^1.4.0" + semver "^7.5.4" + ts-api-utils "^1.0.1" + "@typescript-eslint/parser@^5.49.0": version "5.49.0" resolved "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.49.0.tgz" @@ -865,11 +904,34 @@ "@typescript-eslint/types" "5.49.0" "@typescript-eslint/visitor-keys" "5.49.0" +"@typescript-eslint/scope-manager@6.13.2": + version "6.13.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-6.13.2.tgz#5fa4e4adace028dafac212c770640b94e7b61052" + integrity sha512-CXQA0xo7z6x13FeDYCgBkjWzNqzBn8RXaE3QVQVIUm74fWJLkJkaHmHdKStrxQllGh6Q4eUGyNpMe0b1hMkXFA== + dependencies: + "@typescript-eslint/types" "6.13.2" + "@typescript-eslint/visitor-keys" "6.13.2" + +"@typescript-eslint/type-utils@6.13.2": + version "6.13.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-6.13.2.tgz#ebec2da14a6bb7122e0fd31eea72a382c39c6102" + integrity sha512-Qr6ssS1GFongzH2qfnWKkAQmMUyZSyOr0W54nZNU1MDfo+U4Mv3XveeLZzadc/yq8iYhQZHYT+eoXJqnACM1tw== + dependencies: + "@typescript-eslint/typescript-estree" "6.13.2" + "@typescript-eslint/utils" "6.13.2" + debug "^4.3.4" + ts-api-utils "^1.0.1" + "@typescript-eslint/types@5.49.0": version "5.49.0" resolved "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.49.0.tgz" integrity sha512-7If46kusG+sSnEpu0yOz2xFv5nRz158nzEXnJFCGVEHWnuzolXKwrH5Bsf9zsNlOQkyZuk0BZKKoJQI+1JPBBg== +"@typescript-eslint/types@6.13.2": + version "6.13.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-6.13.2.tgz#c044aac24c2f6cefb8e921e397acad5417dd0ae6" + integrity sha512-7sxbQ+EMRubQc3wTfTsycgYpSujyVbI1xw+3UMRUcrhSy+pN09y/lWzeKDbvhoqcRbHdc+APLs/PWYi/cisLPg== + "@typescript-eslint/typescript-estree@5.49.0": version "5.49.0" resolved "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.49.0.tgz" @@ -883,6 +945,32 @@ semver "^7.3.7" tsutils "^3.21.0" +"@typescript-eslint/typescript-estree@6.13.2": + version "6.13.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-6.13.2.tgz#ae556ee154c1acf025b48d37c3ef95a1d55da258" + integrity sha512-SuD8YLQv6WHnOEtKv8D6HZUzOub855cfPnPMKvdM/Bh1plv1f7Q/0iFUDLKKlxHcEstQnaUU4QZskgQq74t+3w== + dependencies: + "@typescript-eslint/types" "6.13.2" + "@typescript-eslint/visitor-keys" "6.13.2" + debug "^4.3.4" + globby "^11.1.0" + is-glob "^4.0.3" + semver "^7.5.4" + ts-api-utils "^1.0.1" + +"@typescript-eslint/utils@6.13.2": + version "6.13.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-6.13.2.tgz#8eb89e53adc6d703a879b131e528807245486f89" + integrity sha512-b9Ptq4eAZUym4idijCRzl61oPCwwREcfDI8xGk751Vhzig5fFZR9CyzDz4Sp/nxSLBYxUPyh4QdIDqWykFhNmQ== + dependencies: + "@eslint-community/eslint-utils" "^4.4.0" + "@types/json-schema" "^7.0.12" + "@types/semver" "^7.5.0" + "@typescript-eslint/scope-manager" "6.13.2" + "@typescript-eslint/types" "6.13.2" + "@typescript-eslint/typescript-estree" "6.13.2" + semver "^7.5.4" + "@typescript-eslint/utils@^5.10.0": version "5.49.0" resolved "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.49.0.tgz" @@ -905,6 +993,14 @@ "@typescript-eslint/types" "5.49.0" eslint-visitor-keys "^3.3.0" +"@typescript-eslint/visitor-keys@6.13.2": + version "6.13.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-6.13.2.tgz#e0a4a80cf842bb08e6127b903284166ac4a5594c" + integrity sha512-OGznFs0eAQXJsp+xSd6k/O1UbFi/K/L7WjqeRoFE7vadjAF9y0uppXhYNQNEqygjou782maGClOoZwPqF0Drlw== + dependencies: + "@typescript-eslint/types" "6.13.2" + eslint-visitor-keys "^3.4.1" + "@zxing/text-encoding@0.9.0": version "0.9.0" resolved "https://registry.npmjs.org/@zxing/text-encoding/-/text-encoding-0.9.0.tgz" @@ -1374,6 +1470,16 @@ color-convert@^2.0.1: dependencies: color-name "~1.1.4" +color-name@1.1.3: + version "1.1.3" + resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" + integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw== + +color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + color-name@1.1.3: version "1.1.3" resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz" @@ -1773,6 +1879,11 @@ eslint-visitor-keys@^3.3.0: resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz" integrity sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA== +eslint-visitor-keys@^3.4.1: + version "3.4.3" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800" + integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag== + eslint@^8.32.0: version "8.33.0" resolved "https://registry.npmjs.org/eslint/-/eslint-8.33.0.tgz" @@ -2132,6 +2243,11 @@ function-bind@^1.1.1, function-bind@^1.1.2: resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz" integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== +function-bind@^1.1.2: + version "1.1.2" + resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz" + integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== + gensync@^1.0.0-beta.2: version "1.0.0-beta.2" resolved "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" @@ -2282,6 +2398,11 @@ grapheme-splitter@^1.0.4: resolved "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz" integrity sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ== +graphemer@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6" + integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag== + has-flag@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" @@ -2338,6 +2459,26 @@ headers-polyfill@^4.0.2: resolved "https://registry.npmjs.org/headers-polyfill/-/headers-polyfill-4.0.2.tgz" integrity sha512-EWGTfnTqAO2L/j5HZgoM/3z82L7necsJ0pO9Tp0X1wil3PDLrkypTBRgVO2ExehEEvUycejZD3FuRaXpZZc3kw== +help-me@^4.0.1: + version "4.2.0" + resolved "https://registry.npmjs.org/help-me/-/help-me-4.2.0.tgz#50712bfd799ff1854ae1d312c36eafcea85b0563" + integrity sha512-TAOnTB8Tz5Dw8penUuzHVrKNKlCIbwwbHnXraNJxPwf8LRtE2HlM84RYuezMFcwOJmoYOCWVDyJ8TQGxn9PgxA== + dependencies: + glob "^8.0.0" + readable-stream "^3.6.0" + +hasown@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz" + integrity sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA== + dependencies: + function-bind "^1.1.2" + +headers-polyfill@^4.0.2: + version "4.0.2" + resolved "https://registry.npmjs.org/headers-polyfill/-/headers-polyfill-4.0.2.tgz" + integrity sha512-EWGTfnTqAO2L/j5HZgoM/3z82L7necsJ0pO9Tp0X1wil3PDLrkypTBRgVO2ExehEEvUycejZD3FuRaXpZZc3kw== + hexoid@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/hexoid/-/hexoid-1.0.0.tgz" @@ -2393,6 +2534,11 @@ ignore@^5.2.0: resolved "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz" integrity sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ== +ignore@^5.2.4: + version "5.3.0" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.0.tgz#67418ae40d34d6999c95ff56016759c718c82f78" + integrity sha512-g7dmpshy+gD7mh88OC9NwSGTKoc3kyLAZQRU1mt53Aw/vnvfXnbC+F/7F7QoYVKbV+KNvJx8wArewKy1vXMtlg== + import-fresh@^3.0.0, import-fresh@^3.2.1: version "3.3.0" resolved "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz" @@ -3723,6 +3869,16 @@ process@^0.11.10: resolved "https://registry.npmjs.org/process/-/process-0.11.10.tgz" integrity sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A== +process-warning@^2.0.0: + version "2.3.1" + resolved "https://registry.npmjs.org/process-warning/-/process-warning-2.3.1.tgz" + integrity sha512-JjBvFEn7MwFbzUDa2SRtKJSsyO0LlER4V/FmwLMhBlXNbGgGxdyFCxIdMDLerWUycsVUyaoM9QFLvppFy4IWaQ== + +process@^0.11.10: + version "0.11.10" + resolved "https://registry.npmjs.org/process/-/process-0.11.10.tgz" + integrity sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A== + prompts@^2.0.1: version "2.4.2" resolved "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz#7b57e73b3a48029ad10ebd44f74b01722a4cb069" @@ -4254,6 +4410,11 @@ tr46@~0.0.3: resolved "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz" integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== +ts-api-utils@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-1.0.3.tgz#f12c1c781d04427313dbac808f453f050e54a331" + integrity sha512-wNMeqtMz5NtwpT/UZGY5alT+VoKdSsOOP/kqHFcUW1P/VRhH2wJ48+DN2WwUliNbQ976ETwDL0Ifd2VVvgonvg== + ts-essentials@^9.4.1: version "9.4.1" resolved "https://registry.npmjs.org/ts-essentials/-/ts-essentials-9.4.1.tgz" @@ -4393,6 +4554,11 @@ url-parse-lax@^3.0.0: dependencies: prepend-http "^2.0.0" +util-deprecate@^1.0.1: + version "1.0.2" + resolved "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== + util@^0.12.3: version "0.12.5" resolved "https://registry.npmjs.org/util/-/util-0.12.5.tgz"