diff --git a/packages/cactus-common/src/main/typescript/exception/coerce-unknown-to-error.ts b/packages/cactus-common/src/main/typescript/exception/coerce-unknown-to-error.ts index cc1e6f2419..bdb9232252 100644 --- a/packages/cactus-common/src/main/typescript/exception/coerce-unknown-to-error.ts +++ b/packages/cactus-common/src/main/typescript/exception/coerce-unknown-to-error.ts @@ -1,4 +1,5 @@ import stringify from "fast-safe-stringify"; +import sanitizeHtml from "sanitize-html"; import { ErrorFromUnknownThrowable } from "./error-from-unknown-throwable"; import { ErrorFromSymbol } from "./error-from-symbol"; @@ -26,10 +27,11 @@ export function coerceUnknownToError(x: unknown): Error { } else if (x instanceof Error) { return x; } else { - const xAsJson = stringify(x, (_, value) => + const xAsJsonUnsafe = stringify(x, (_, value) => typeof value === "bigint" ? value.toString() + "n" : value, ); - return new ErrorFromUnknownThrowable(xAsJson); + const xAsJsonSanitized = sanitizeHtml(xAsJsonUnsafe); + return new ErrorFromUnknownThrowable(xAsJsonSanitized); } } diff --git a/packages/cactus-core/package.json b/packages/cactus-core/package.json index d1f0dd44e8..1913d39c06 100644 --- a/packages/cactus-core/package.json +++ b/packages/cactus-core/package.json @@ -56,6 +56,7 @@ "express-jwt-authz": "2.4.1", "express-openapi-validator": "5.0.4", "http-errors": "2.0.0", + "http-errors-enhanced": "1.1.2", "run-time-error-cjs": "1.4.0", "safe-stable-stringify": "2.4.3", "typescript-optional": "2.0.1" @@ -63,6 +64,7 @@ "devDependencies": { "@types/express": "4.17.19", "@types/http-errors": "2.0.2", + "node-mocks-http": "1.14.0", "uuid": "8.3.2" }, "engines": { diff --git a/packages/cactus-core/src/main/typescript/web-services/handle-rest-endpoint-exception.ts b/packages/cactus-core/src/main/typescript/web-services/handle-rest-endpoint-exception.ts index 66d291df8d..56b02462bc 100644 --- a/packages/cactus-core/src/main/typescript/web-services/handle-rest-endpoint-exception.ts +++ b/packages/cactus-core/src/main/typescript/web-services/handle-rest-endpoint-exception.ts @@ -1,12 +1,13 @@ import { Logger, createRuntimeErrorWithCause, + safeStringifyException, } from "@hyperledger/cactus-common"; import type { Response } from "express"; import createHttpError from "http-errors"; /** - * An interface describing the object contaiing the contextual information needed by the + * An interface describing the object containing the contextual information needed by the * `#handleRestEndpointException()` method to perform its duties. * * @param ctx - An object containing options for handling the REST endpoint exception. @@ -35,39 +36,46 @@ export interface IHandleRestEndpointExceptionOptions { * * @param ctx - An object containing options for handling the REST endpoint exception. */ -export function handleRestEndpointException( +export async function handleRestEndpointException( ctx: Readonly, -): void { +): Promise { + const errorAsSanitizedJson = safeStringifyException(ctx.error); + + const { identifierByCodes, INTERNAL_SERVER_ERROR } = await import( + "http-errors-enhanced" + ); + if (createHttpError.isHttpError(ctx.error)) { ctx.res.status(ctx.error.statusCode); // Log either an error or a debug message depending on what the statusCode is // For 5xx errors we treat it as a production bug that needs to be fixed on // our side and for everything else we treat it a user error and debug log it. - if (ctx.error.statusCode >= 500) { - ctx.log.debug(ctx.errorMsg, ctx.error); + if (ctx.error.statusCode >= INTERNAL_SERVER_ERROR) { + ctx.log.debug(ctx.errorMsg, errorAsSanitizedJson); } else { - ctx.log.error(ctx.errorMsg, ctx.error); + ctx.log.error(ctx.errorMsg, errorAsSanitizedJson); } // If the `expose` property is set to true it implies that we can safely // expose the contents of the exception to the calling client. if (ctx.error.expose) { ctx.res.json({ - message: ctx.error.message, - error: ctx.error, + message: identifierByCodes[ctx.error.statusCode], + error: errorAsSanitizedJson, }); } } else { // If the exception was not an http-error then we assume it was an internal // error (e.g. same behavior as if we had received an HTTP 500 statusCode) - ctx.log.error(ctx.errorMsg, ctx.error); + ctx.log.error(ctx.errorMsg, errorAsSanitizedJson); const rex = createRuntimeErrorWithCause(ctx.errorMsg, ctx.error); + const sanitizedJsonRex = safeStringifyException(rex); - ctx.res.status(500).json({ - message: "Internal Server Error", - error: rex, + ctx.res.status(INTERNAL_SERVER_ERROR).json({ + message: identifierByCodes[INTERNAL_SERVER_ERROR], + error: sanitizedJsonRex, }); } } diff --git a/packages/cactus-core/src/test/typescript/unit/handle-rest-endpoint-exception.test.ts b/packages/cactus-core/src/test/typescript/unit/handle-rest-endpoint-exception.test.ts new file mode 100644 index 0000000000..35fd6218d5 --- /dev/null +++ b/packages/cactus-core/src/test/typescript/unit/handle-rest-endpoint-exception.test.ts @@ -0,0 +1,211 @@ +import "jest-extended"; +import createHttpError from "http-errors"; +import { createResponse } from "node-mocks-http"; + +import { safeStringifyException } from "@hyperledger/cactus-common"; + +import { + handleRestEndpointException, + IHandleRestEndpointExceptionOptions, +} from "../../../main/typescript/public-api"; // replace with the correct path to your module + +import { LoggerProvider } from "@hyperledger/cactus-common"; + +const log = LoggerProvider.getOrCreate({ + label: "handle-rest-endpoint-exception.test.ts", + level: "DEBUG", +}); + +describe("handleRestEndpointException", () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + it("should handle HttpError with statusCode >= 500", async () => { + const mockResponse = createResponse(); + + const mockOptions: IHandleRestEndpointExceptionOptions = { + errorMsg: "Test error message", + log: jest.mocked(log), // Provide a mock logger if needed + error: new Error("Test error"), // Provide appropriate error objects for testing + res: mockResponse, + }; + + const mockHttpError = createHttpError(500, "Test HTTP error", { + expose: true, + }); + + const errorAsSanitizedJson = safeStringifyException(mockHttpError); + const spyLogDebug = jest.spyOn(mockOptions.log, "debug"); + const spyStatus = jest.spyOn(mockResponse, "status"); + const spyJson = jest.spyOn(mockResponse, "json"); + + await handleRestEndpointException({ ...mockOptions, error: mockHttpError }); + + expect(spyStatus).toHaveBeenCalledWith(500); + + expect(spyLogDebug).toHaveBeenCalledWith( + mockOptions.errorMsg, + errorAsSanitizedJson, + ); + + expect(spyJson).toHaveBeenCalledWith({ + message: "InternalServerError", + error: errorAsSanitizedJson, + }); + }); + + it("should handle HttpError with statusCode < 500", async () => { + const mockResponse = createResponse(); + + const mockOptions: IHandleRestEndpointExceptionOptions = { + errorMsg: "Test error message", + log: jest.mocked(log), // Provide a mock logger if needed + error: new Error("Test error"), // Provide appropriate error objects for testing + res: mockResponse, + }; + + const mockHttpError = createHttpError(404, "Test HTTP error", { + expose: true, + }); + + const errorAsSanitizedJson = safeStringifyException(mockHttpError); + const spyLogError = jest.spyOn(mockOptions.log, "error"); + const spyStatus = jest.spyOn(mockResponse, "status"); + const spyJson = jest.spyOn(mockResponse, "json"); + await handleRestEndpointException({ ...mockOptions, error: mockHttpError }); + + expect(spyStatus).toHaveBeenCalledWith(404); + expect(spyLogError).toHaveBeenCalledWith( + mockOptions.errorMsg, + errorAsSanitizedJson, + ); + + expect(spyJson).toHaveBeenCalledWith({ + message: "NotFound", + error: errorAsSanitizedJson, + }); + }); + + it("should handle non-HttpError", async () => { + const mockResponse = createResponse(); + + const mockError = new Error("An unexpected exception. Ha!"); + + const mockOptions: IHandleRestEndpointExceptionOptions = { + errorMsg: "Test error message", + log: jest.mocked(log), // Provide a mock logger if needed + error: mockError, + res: mockResponse, + }; + + const mockErrorJson = safeStringifyException(mockError); + const spyLoggerFn = jest.spyOn(mockOptions.log, "error"); + const spyStatus = jest.spyOn(mockResponse, "status"); + const spyJson = jest.spyOn(mockResponse, "json"); + + await handleRestEndpointException({ ...mockOptions, error: mockError }); + + expect(spyStatus).toHaveBeenCalledWith(500); + + expect(spyLoggerFn).toHaveBeenCalledWith( + mockOptions.errorMsg, + mockErrorJson, + ); + + expect(spyJson).toHaveBeenCalledOnce(); + + const mostRecentCall = spyJson.mock.lastCall; + expect(mostRecentCall).toBeTruthy(); + + expect(mostRecentCall?.[0].message).toBeString(); + expect(mostRecentCall?.[0].message).toEqual("InternalServerError"); + expect(mostRecentCall?.[0].error).toMatch( + /RuntimeError: Test error message(.*)An unexpected exception. Ha!/, + ); + }); + + it("should escape malicious payloads in exception messages", async () => { + const mockResponse = createResponse(); + + const dummyXssPayload = ``; + const mockError = new Error(dummyXssPayload); + + const mockOptions: IHandleRestEndpointExceptionOptions = { + errorMsg: "Test error message", + log: jest.mocked(log), // Provide a mock logger if needed + error: mockError, + res: mockResponse, + }; + + const mockErrorJson = safeStringifyException(mockError); + const spyLoggerFn = jest.spyOn(mockOptions.log, "error"); + const spyStatus = jest.spyOn(mockResponse, "status"); + const spyJson = jest.spyOn(mockResponse, "json"); + + await handleRestEndpointException({ ...mockOptions, error: mockError }); + + expect(spyStatus).toHaveBeenCalledWith(500); + + expect(spyLoggerFn).toHaveBeenCalledWith( + mockOptions.errorMsg, + mockErrorJson, + ); + + expect(spyJson).toHaveBeenCalledOnce(); + + const mostRecentCall = spyJson.mock.lastCall; + expect(mostRecentCall).toBeTruthy(); + + expect(mostRecentCall?.[0].message).toBeString(); + expect(mostRecentCall?.[0].message).toEqual("InternalServerError"); + expect(mostRecentCall?.[0].error).toMatch( + /RuntimeError: Test error message(.*)/, + ); + expect(mostRecentCall?.[0].error).not.toMatch( + /.*stealAndUploadPrivateKeys.*/, + ); + }); + + it("should escape malicious payloads in strings thrown", async () => { + const mockResponse = createResponse(); + + const dummyXssPayload = ``; + const mockError = dummyXssPayload; + + const mockOptions: IHandleRestEndpointExceptionOptions = { + errorMsg: "Test error message", + log: jest.mocked(log), // Provide a mock logger if needed + error: mockError, + res: mockResponse, + }; + + const mockErrorJson = safeStringifyException(mockError); + const spyLoggerFn = jest.spyOn(mockOptions.log, "error"); + const spyStatus = jest.spyOn(mockResponse, "status"); + const spyJson = jest.spyOn(mockResponse, "json"); + + await handleRestEndpointException({ ...mockOptions, error: mockError }); + + expect(spyStatus).toHaveBeenCalledWith(500); + + expect(spyLoggerFn).toHaveBeenCalledWith( + mockOptions.errorMsg, + mockErrorJson, + ); + + expect(spyJson).toHaveBeenCalledOnce(); + + const mostRecentCall = spyJson.mock.lastCall; + expect(mostRecentCall).toBeTruthy(); + + expect(mostRecentCall?.[0].message).toBeString(); + expect(mostRecentCall?.[0].message).toEqual("InternalServerError"); + expect(mostRecentCall?.[0].error).toMatch( + /RuntimeError: Test error message(.*)/, + ); + expect(mostRecentCall?.[0].error).not.toMatch( + /.*stealAndUploadPrivateKeys.*/, + ); + }); +}); diff --git a/packages/cactus-plugin-keychain-memory/src/main/typescript/web-services/delete-keychain-entry-endpoint-v1.ts b/packages/cactus-plugin-keychain-memory/src/main/typescript/web-services/delete-keychain-entry-endpoint-v1.ts index f247b1b294..33f9ab3874 100644 --- a/packages/cactus-plugin-keychain-memory/src/main/typescript/web-services/delete-keychain-entry-endpoint-v1.ts +++ b/packages/cactus-plugin-keychain-memory/src/main/typescript/web-services/delete-keychain-entry-endpoint-v1.ts @@ -87,6 +87,7 @@ export class DeleteKeychainEntryV1Endpoint implements IWebServiceEndpoint { } public async handleRequest(req: Request, res: Response): Promise { + const { log } = this; const fnTag = `${this.className}#handleRequest()`; const reqTag = `${this.getVerbLowerCase()} - ${this.getPath()}`; this.log.debug(reqTag); @@ -96,7 +97,7 @@ export class DeleteKeychainEntryV1Endpoint implements IWebServiceEndpoint { res.json(resBody); } catch (ex) { const errorMsg = `${reqTag} ${fnTag} Failed to deploy contract:`; - handleRestEndpointException({ errorMsg, log: this.log, error: ex, res }); + await handleRestEndpointException({ errorMsg, log, error: ex, res }); } } } diff --git a/packages/cactus-plugin-keychain-memory/src/main/typescript/web-services/get-keychain-entry-endpoint-v1.ts b/packages/cactus-plugin-keychain-memory/src/main/typescript/web-services/get-keychain-entry-endpoint-v1.ts index ee8bb4fce4..87ed6b2ce8 100644 --- a/packages/cactus-plugin-keychain-memory/src/main/typescript/web-services/get-keychain-entry-endpoint-v1.ts +++ b/packages/cactus-plugin-keychain-memory/src/main/typescript/web-services/get-keychain-entry-endpoint-v1.ts @@ -87,6 +87,7 @@ export class GetKeychainEntryV1Endpoint implements IWebServiceEndpoint { } public async handleRequest(req: Request, res: Response): Promise { + const { log } = this; const fnTag = `${this.className}#handleRequest()`; const reqTag = `${this.getVerbLowerCase()} - ${this.getPath()}`; this.log.debug(reqTag); @@ -96,7 +97,7 @@ export class GetKeychainEntryV1Endpoint implements IWebServiceEndpoint { res.json({ key, value }); } catch (ex) { const errorMsg = `${reqTag} ${fnTag} Failed to deploy contract:`; - handleRestEndpointException({ errorMsg, log: this.log, error: ex, res }); + await handleRestEndpointException({ errorMsg, log, error: ex, res }); } } } diff --git a/packages/cactus-plugin-keychain-memory/src/main/typescript/web-services/has-keychain-entry-endpoint-v1.ts b/packages/cactus-plugin-keychain-memory/src/main/typescript/web-services/has-keychain-entry-endpoint-v1.ts index 4b632f956a..8d0452a436 100644 --- a/packages/cactus-plugin-keychain-memory/src/main/typescript/web-services/has-keychain-entry-endpoint-v1.ts +++ b/packages/cactus-plugin-keychain-memory/src/main/typescript/web-services/has-keychain-entry-endpoint-v1.ts @@ -87,6 +87,7 @@ export class HasKeychainEntryV1Endpoint implements IWebServiceEndpoint { } public async handleRequest(req: Request, res: Response): Promise { + const { log } = this; const fnTag = `${this.className}#handleRequest()`; const reqTag = `${this.getVerbLowerCase()} - ${this.getPath()}`; this.log.debug(reqTag); @@ -103,7 +104,7 @@ export class HasKeychainEntryV1Endpoint implements IWebServiceEndpoint { res.json(resBody); } catch (ex) { const errorMsg = `${reqTag} ${fnTag} Failed to deploy contract:`; - handleRestEndpointException({ errorMsg, log: this.log, error: ex, res }); + await handleRestEndpointException({ errorMsg, log, error: ex, res }); } } } diff --git a/packages/cactus-plugin-keychain-memory/src/main/typescript/web-services/set-keychain-entry-endpoint-v1.ts b/packages/cactus-plugin-keychain-memory/src/main/typescript/web-services/set-keychain-entry-endpoint-v1.ts index fdf32faac6..743221d45c 100644 --- a/packages/cactus-plugin-keychain-memory/src/main/typescript/web-services/set-keychain-entry-endpoint-v1.ts +++ b/packages/cactus-plugin-keychain-memory/src/main/typescript/web-services/set-keychain-entry-endpoint-v1.ts @@ -87,6 +87,7 @@ export class SetKeychainEntryV1Endpoint implements IWebServiceEndpoint { } public async handleRequest(req: Request, res: Response): Promise { + const { log } = this; const fnTag = `${this.className}#handleRequest()`; const reqTag = `${this.getVerbLowerCase()} - ${this.getPath()}`; this.log.debug(reqTag); @@ -96,7 +97,7 @@ export class SetKeychainEntryV1Endpoint implements IWebServiceEndpoint { res.json(resBody); } catch (ex) { const errorMsg = `${reqTag} ${fnTag} Failed to deploy contract:`; - handleRestEndpointException({ errorMsg, log: this.log, error: ex, res }); + await handleRestEndpointException({ errorMsg, log, error: ex, res }); } } } diff --git a/packages/cactus-plugin-ledger-connector-besu/src/main/typescript/web-services/deploy-contract-solidity-bytecode-endpoint.ts b/packages/cactus-plugin-ledger-connector-besu/src/main/typescript/web-services/deploy-contract-solidity-bytecode-endpoint.ts index 92b0d72e6b..5d14aaef7e 100644 --- a/packages/cactus-plugin-ledger-connector-besu/src/main/typescript/web-services/deploy-contract-solidity-bytecode-endpoint.ts +++ b/packages/cactus-plugin-ledger-connector-besu/src/main/typescript/web-services/deploy-contract-solidity-bytecode-endpoint.ts @@ -89,6 +89,7 @@ export class DeployContractSolidityBytecodeEndpoint } public async handleRequest(req: Request, res: Response): Promise { + const { log } = this; const fnTag = `${this.className}#handleRequest()`; const reqTag = `${this.getVerbLowerCase()} - ${this.getPath()}`; this.log.debug(reqTag); @@ -98,7 +99,7 @@ export class DeployContractSolidityBytecodeEndpoint res.json(resBody); } catch (ex) { const errorMsg = `${reqTag} ${fnTag} Failed to deploy contract:`; - handleRestEndpointException({ errorMsg, log: this.log, error: ex, res }); + await handleRestEndpointException({ errorMsg, log, error: ex, res }); } } } diff --git a/yarn.lock b/yarn.lock index 0f17b6cc47..abc0e6a168 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6814,6 +6814,8 @@ __metadata: express-jwt-authz: 2.4.1 express-openapi-validator: 5.0.4 http-errors: 2.0.0 + http-errors-enhanced: 1.1.2 + node-mocks-http: 1.14.0 run-time-error-cjs: 1.4.0 safe-stable-stringify: 2.4.3 typescript-optional: 2.0.1 @@ -14940,7 +14942,7 @@ __metadata: languageName: node linkType: hard -"accepts@npm:^1.3.5, accepts@npm:~1.3.7, accepts@npm:~1.3.8": +"accepts@npm:^1.3.5, accepts@npm:^1.3.7, accepts@npm:~1.3.7, accepts@npm:~1.3.8": version: 1.3.8 resolution: "accepts@npm:1.3.8" dependencies: @@ -19651,7 +19653,7 @@ __metadata: languageName: node linkType: hard -"content-disposition@npm:0.5.4": +"content-disposition@npm:0.5.4, content-disposition@npm:^0.5.3": version: 0.5.4 resolution: "content-disposition@npm:0.5.4" dependencies: @@ -21594,7 +21596,7 @@ __metadata: languageName: node linkType: hard -"depd@npm:^1.1.2, depd@npm:~1.1.2": +"depd@npm:^1.1.0, depd@npm:^1.1.2, depd@npm:~1.1.2": version: 1.1.2 resolution: "depd@npm:1.1.2" checksum: 6b406620d269619852885ce15965272b829df6f409724415e0002c8632ab6a8c0a08ec1f0bd2add05dc7bd7507606f7e2cc034fa24224ab829580040b835ecd9 @@ -26167,7 +26169,7 @@ __metadata: languageName: node linkType: hard -"fresh@npm:0.5.2": +"fresh@npm:0.5.2, fresh@npm:^0.5.2": version: 0.5.2 resolution: "fresh@npm:0.5.2" checksum: 13ea8b08f91e669a64e3ba3a20eb79d7ca5379a81f1ff7f4310d54e2320645503cc0c78daedc93dfb6191287295f6479544a649c64d8e41a1c0fb0c221552346 @@ -28216,6 +28218,13 @@ __metadata: languageName: node linkType: hard +"http-errors-enhanced@npm:1.1.2": + version: 1.1.2 + resolution: "http-errors-enhanced@npm:1.1.2" + checksum: 1bc38968cc4a0dcb6a2d25e557d0321f3fa17da2f1a6b898158fa3b9bb103fd5374b38e60d87926e5adf9310fd7b6136023754b0db1489a6e80d6d242375ba0f + languageName: node + linkType: hard + "http-errors@npm:1.6.3, http-errors@npm:~1.6.2": version: 1.6.3 resolution: "http-errors@npm:1.6.3" @@ -34443,6 +34452,13 @@ __metadata: languageName: node linkType: hard +"merge-descriptors@npm:^1.0.1": + version: 1.0.3 + resolution: "merge-descriptors@npm:1.0.3" + checksum: 52117adbe0313d5defa771c9993fe081e2d2df9b840597e966aadafde04ae8d0e3da46bac7ca4efc37d4d2b839436582659cd49c6a43eacb3fe3050896a105d1 + languageName: node + linkType: hard + "merge-options@npm:^3.0.4": version: 3.0.4 resolution: "merge-options@npm:3.0.4" @@ -34498,7 +34514,7 @@ __metadata: languageName: node linkType: hard -"methods@npm:~1.1.2": +"methods@npm:^1.1.2, methods@npm:~1.1.2": version: 1.1.2 resolution: "methods@npm:1.1.2" checksum: 0917ff4041fa8e2f2fda5425a955fe16ca411591fbd123c0d722fcf02b73971ed6f764d85f0a6f547ce49ee0221ce2c19a5fa692157931cecb422984f1dcd13a @@ -34602,7 +34618,7 @@ __metadata: languageName: node linkType: hard -"mime@npm:1.6.0, mime@npm:^1.4.1": +"mime@npm:1.6.0, mime@npm:^1.3.4, mime@npm:^1.4.1": version: 1.6.0 resolution: "mime@npm:1.6.0" bin: @@ -35941,6 +35957,24 @@ __metadata: languageName: node linkType: hard +"node-mocks-http@npm:1.14.0": + version: 1.14.0 + resolution: "node-mocks-http@npm:1.14.0" + dependencies: + accepts: ^1.3.7 + content-disposition: ^0.5.3 + depd: ^1.1.0 + fresh: ^0.5.2 + merge-descriptors: ^1.0.1 + methods: ^1.1.2 + mime: ^1.3.4 + parseurl: ^1.3.3 + range-parser: ^1.2.0 + type-is: ^1.6.18 + checksum: 2cbf2d2e0dcbf6935e2f2c7e3b3bbea4b60cf043f8cf846856bf177e6570bf7b5629311b97f673d290cf51ed8e7e7c725f9a7a43ad224df423ed92389ac9ff8f + languageName: node + linkType: hard + "node-notifier@npm:8.0.2": version: 8.0.2 resolution: "node-notifier@npm:8.0.2" @@ -40375,7 +40409,7 @@ __metadata: languageName: node linkType: hard -"range-parser@npm:^1.2.1, range-parser@npm:~1.2.1": +"range-parser@npm:^1.2.0, range-parser@npm:^1.2.1, range-parser@npm:~1.2.1": version: 1.2.1 resolution: "range-parser@npm:1.2.1" checksum: 0a268d4fea508661cf5743dfe3d5f47ce214fd6b7dec1de0da4d669dd4ef3d2144468ebe4179049eff253d9d27e719c88dae55be64f954e80135a0cada804ec9 @@ -46647,7 +46681,7 @@ __metadata: languageName: node linkType: hard -"type-is@npm:^1.6.4, type-is@npm:~1.6.17, type-is@npm:~1.6.18": +"type-is@npm:^1.6.18, type-is@npm:^1.6.4, type-is@npm:~1.6.17, type-is@npm:~1.6.18": version: 1.6.18 resolution: "type-is@npm:1.6.18" dependencies: