diff --git a/packages/credential-provider-imds/package.json b/packages/credential-provider-imds/package.json index 61cd1ca94aa3..d047a9575529 100644 --- a/packages/credential-provider-imds/package.json +++ b/packages/credential-provider-imds/package.json @@ -29,6 +29,7 @@ "@types/jest": "^26.0.4", "@types/node": "^10.0.0", "jest": "^26.1.0", + "nock": "^13.0.2", "typescript": "~3.8.3" }, "types": "./dist/cjs/index.d.ts" diff --git a/packages/credential-provider-imds/src/fromContainerMetadata.spec.ts b/packages/credential-provider-imds/src/fromContainerMetadata.spec.ts index 580947f1cbba..74cdbf2d4d3e 100644 --- a/packages/credential-provider-imds/src/fromContainerMetadata.spec.ts +++ b/packages/credential-provider-imds/src/fromContainerMetadata.spec.ts @@ -4,21 +4,21 @@ import { ENV_CMDS_RELATIVE_URI, fromContainerMetadata } from "./fromContainerMetadata"; -import { httpGet } from "./remoteProvider/httpGet"; +import { httpRequest } from "./remoteProvider/httpRequest"; import { fromImdsCredentials, ImdsCredentials } from "./remoteProvider/ImdsCredentials"; -const mockHttpGet = httpGet; -jest.mock("./remoteProvider/httpGet", () => ({ httpGet: jest.fn() })); +const mockHttpRequest = httpRequest; +jest.mock("./remoteProvider/httpRequest", () => ({ httpRequest: jest.fn() })); const relativeUri = process.env[ENV_CMDS_RELATIVE_URI]; const fullUri = process.env[ENV_CMDS_FULL_URI]; const authToken = process.env[ENV_CMDS_AUTH_TOKEN]; beforeEach(() => { - mockHttpGet.mockReset(); + mockHttpRequest.mockReset(); delete process.env[ENV_CMDS_RELATIVE_URI]; delete process.env[ENV_CMDS_FULL_URI]; delete process.env[ENV_CMDS_AUTH_TOKEN]; @@ -53,12 +53,12 @@ describe("fromContainerMetadata", () => { const token = "Basic abcd"; process.env[ENV_CMDS_FULL_URI] = "http://localhost:8080/path"; process.env[ENV_CMDS_AUTH_TOKEN] = token; - mockHttpGet.mockReturnValue(Promise.resolve(JSON.stringify(creds))); + mockHttpRequest.mockReturnValue(Promise.resolve(JSON.stringify(creds))); await fromContainerMetadata()(); - expect(mockHttpGet.mock.calls.length).toBe(1); - const [options = {}] = mockHttpGet.mock.calls[0]; + expect(mockHttpRequest.mock.calls.length).toBe(1); + const [options = {}] = mockHttpRequest.mock.calls[0]; expect(options.headers).toMatchObject({ Authorization: token }); @@ -70,7 +70,7 @@ describe("fromContainerMetadata", () => { }); it("should resolve credentials by fetching them from the container metadata service", async () => { - mockHttpGet.mockReturnValue(Promise.resolve(JSON.stringify(creds))); + mockHttpRequest.mockReturnValue(Promise.resolve(JSON.stringify(creds))); expect(await fromContainerMetadata()()).toEqual( fromImdsCredentials(creds) @@ -80,38 +80,42 @@ describe("fromContainerMetadata", () => { it("should retry the fetching operation up to maxRetries times", async () => { const maxRetries = 5; for (let i = 0; i < maxRetries - 1; i++) { - mockHttpGet.mockReturnValueOnce(Promise.reject("No!")); + mockHttpRequest.mockReturnValueOnce(Promise.reject("No!")); } - mockHttpGet.mockReturnValueOnce(Promise.resolve(JSON.stringify(creds))); + mockHttpRequest.mockReturnValueOnce( + Promise.resolve(JSON.stringify(creds)) + ); expect(await fromContainerMetadata({ maxRetries })()).toEqual( fromImdsCredentials(creds) ); - expect(mockHttpGet.mock.calls.length).toEqual(maxRetries); + expect(mockHttpRequest.mock.calls.length).toEqual(maxRetries); }); it("should retry responses that receive invalid response values", async () => { for (let key of Object.keys(creds)) { const invalidCreds: any = { ...creds }; delete invalidCreds[key]; - mockHttpGet.mockReturnValueOnce( + mockHttpRequest.mockReturnValueOnce( Promise.resolve(JSON.stringify(invalidCreds)) ); } - mockHttpGet.mockReturnValueOnce(Promise.resolve(JSON.stringify(creds))); + mockHttpRequest.mockReturnValueOnce( + Promise.resolve(JSON.stringify(creds)) + ); await fromContainerMetadata({ maxRetries: 100 })(); - expect(mockHttpGet.mock.calls.length).toEqual( + expect(mockHttpRequest.mock.calls.length).toEqual( Object.keys(creds).length + 1 ); }); - it("should pass relevant configuration to httpGet", async () => { + it("should pass relevant configuration to httpRequest", async () => { const timeout = Math.ceil(Math.random() * 1000); - mockHttpGet.mockReturnValue(Promise.resolve(JSON.stringify(creds))); + mockHttpRequest.mockReturnValue(Promise.resolve(JSON.stringify(creds))); await fromContainerMetadata({ timeout })(); - expect(mockHttpGet.mock.calls.length).toEqual(1); - expect(mockHttpGet.mock.calls[0][0]).toEqual({ + expect(mockHttpRequest.mock.calls.length).toEqual(1); + expect(mockHttpRequest.mock.calls[0][0]).toEqual({ hostname: "169.254.170.2", path: process.env[ENV_CMDS_RELATIVE_URI], timeout @@ -120,20 +124,20 @@ describe("fromContainerMetadata", () => { }); describe(ENV_CMDS_FULL_URI, () => { - it("should pass relevant configuration to httpGet", async () => { + it("should pass relevant configuration to httpRequest", async () => { process.env[ENV_CMDS_FULL_URI] = "http://localhost:8080/path"; const timeout = Math.ceil(Math.random() * 1000); - mockHttpGet.mockReturnValue(Promise.resolve(JSON.stringify(creds))); + mockHttpRequest.mockReturnValue(Promise.resolve(JSON.stringify(creds))); await fromContainerMetadata({ timeout })(); - expect(mockHttpGet.mock.calls.length).toEqual(1); + expect(mockHttpRequest.mock.calls.length).toEqual(1); const { protocol, hostname, path, port, timeout: actualTimeout - } = mockHttpGet.mock.calls[0][0]; + } = mockHttpRequest.mock.calls[0][0]; expect(protocol).toBe("http:"); expect(hostname).toBe("localhost"); expect(path).toBe("/path"); @@ -146,10 +150,10 @@ describe("fromContainerMetadata", () => { process.env[ENV_CMDS_FULL_URI] = "http://localhost:8080/path"; const timeout = Math.ceil(Math.random() * 1000); - mockHttpGet.mockReturnValue(Promise.resolve(JSON.stringify(creds))); + mockHttpRequest.mockReturnValue(Promise.resolve(JSON.stringify(creds))); await fromContainerMetadata({ timeout })(); - expect(mockHttpGet.mock.calls.length).toEqual(1); - expect(mockHttpGet.mock.calls[0][0]).toEqual({ + expect(mockHttpRequest.mock.calls.length).toEqual(1); + expect(mockHttpRequest.mock.calls[0][0]).toEqual({ hostname: "169.254.170.2", path: "foo", timeout diff --git a/packages/credential-provider-imds/src/fromContainerMetadata.ts b/packages/credential-provider-imds/src/fromContainerMetadata.ts index 888180e74745..c4a0e12ecb7c 100644 --- a/packages/credential-provider-imds/src/fromContainerMetadata.ts +++ b/packages/credential-provider-imds/src/fromContainerMetadata.ts @@ -3,7 +3,7 @@ import { RemoteProviderInit, providerConfigFromInit } from "./remoteProvider/RemoteProviderInit"; -import { httpGet } from "./remoteProvider/httpGet"; +import { httpRequest } from "./remoteProvider/httpRequest"; import { fromImdsCredentials, isImdsCredentials @@ -53,7 +53,7 @@ function requestFromEcsImds( options.headers = headers; } - return httpGet({ + return httpRequest({ ...options, timeout }).then(buffer => buffer.toString()); diff --git a/packages/credential-provider-imds/src/fromInstanceMetadata.spec.ts b/packages/credential-provider-imds/src/fromInstanceMetadata.spec.ts index 84d1fbfa62ec..1871a28c9402 100644 --- a/packages/credential-provider-imds/src/fromInstanceMetadata.spec.ts +++ b/packages/credential-provider-imds/src/fromInstanceMetadata.spec.ts @@ -1,5 +1,5 @@ import { fromInstanceMetadata } from "./fromInstanceMetadata"; -import { httpGet } from "./remoteProvider/httpGet"; +import { httpRequest } from "./remoteProvider/httpRequest"; import { fromImdsCredentials, isImdsCredentials @@ -8,7 +8,7 @@ import { providerConfigFromInit } from "./remoteProvider/RemoteProviderInit"; import { retry } from "./remoteProvider/retry"; import { ProviderError } from "@aws-sdk/property-provider"; -jest.mock("./remoteProvider/httpGet"); +jest.mock("./remoteProvider/httpRequest"); jest.mock("./remoteProvider/ImdsCredentials"); jest.mock("./remoteProvider/retry"); jest.mock("./remoteProvider/RemoteProviderInit"); @@ -18,7 +18,7 @@ describe("fromInstanceMetadata", () => { const mockMaxRetries = 3; const mockProfile = "foo"; - const mockHttpGetOptions = { + const mockHttpRequestOptions = { host: "169.254.169.254", path: "/latest/meta-data/iam/security-credentials/", timeout: mockTimeout @@ -51,7 +51,7 @@ describe("fromInstanceMetadata", () => { }); it("gets profile name from IMDS, and passes profile name to fetch credentials", async () => { - (httpGet as jest.Mock) + (httpRequest as jest.Mock) .mockResolvedValueOnce(mockProfile) .mockResolvedValueOnce(JSON.stringify(mockImdsCreds)); @@ -59,16 +59,16 @@ describe("fromInstanceMetadata", () => { (fromImdsCredentials as jest.Mock).mockReturnValue(mockCreds); await expect(fromInstanceMetadata()()).resolves.toEqual(mockCreds); - expect(httpGet).toHaveBeenCalledTimes(2); - expect(httpGet).toHaveBeenNthCalledWith(1, mockHttpGetOptions); - expect(httpGet).toHaveBeenNthCalledWith(2, { - ...mockHttpGetOptions, - path: `${mockHttpGetOptions.path}${mockProfile}` + expect(httpRequest).toHaveBeenCalledTimes(2); + expect(httpRequest).toHaveBeenNthCalledWith(1, mockHttpRequestOptions); + expect(httpRequest).toHaveBeenNthCalledWith(2, { + ...mockHttpRequestOptions, + path: `${mockHttpRequestOptions.path}${mockProfile}` }); }); it("trims profile returned name from IMDS", async () => { - (httpGet as jest.Mock) + (httpRequest as jest.Mock) .mockResolvedValueOnce(" " + mockProfile + " ") .mockResolvedValueOnce(JSON.stringify(mockImdsCreds)); @@ -76,11 +76,11 @@ describe("fromInstanceMetadata", () => { (fromImdsCredentials as jest.Mock).mockReturnValue(mockCreds); await expect(fromInstanceMetadata()()).resolves.toEqual(mockCreds); - expect(httpGet).toHaveBeenCalledTimes(2); - expect(httpGet).toHaveBeenNthCalledWith(1, mockHttpGetOptions); - expect(httpGet).toHaveBeenNthCalledWith(2, { - ...mockHttpGetOptions, - path: `${mockHttpGetOptions.path}${mockProfile}` + expect(httpRequest).toHaveBeenCalledTimes(2); + expect(httpRequest).toHaveBeenNthCalledWith(1, mockHttpRequestOptions); + expect(httpRequest).toHaveBeenNthCalledWith(2, { + ...mockHttpRequestOptions, + path: `${mockHttpRequestOptions.path}${mockProfile}` }); }); @@ -117,7 +117,7 @@ describe("fromInstanceMetadata", () => { }); it("throws ProviderError if credentials returned are incorrect", async () => { - (httpGet as jest.Mock) + (httpRequest as jest.Mock) .mockResolvedValueOnce(mockProfile) .mockResolvedValueOnce(JSON.stringify(mockImdsCreds)); @@ -130,7 +130,7 @@ describe("fromInstanceMetadata", () => { ) ); expect(retry).toHaveBeenCalledTimes(2); - expect(httpGet).toHaveBeenCalledTimes(2); + expect(httpRequest).toHaveBeenCalledTimes(2); expect(isImdsCredentials).toHaveBeenCalledTimes(1); expect(isImdsCredentials).toHaveBeenCalledWith(mockImdsCreds); expect(fromImdsCredentials).not.toHaveBeenCalled(); @@ -138,29 +138,29 @@ describe("fromInstanceMetadata", () => { it("throws Error if requestFromEc2Imds for profile fails", async () => { const mockError = new Error("profile not found"); - (httpGet as jest.Mock).mockRejectedValueOnce(mockError); + (httpRequest as jest.Mock).mockRejectedValueOnce(mockError); (retry as jest.Mock).mockImplementation((fn: any) => fn()); await expect(fromInstanceMetadata()()).rejects.toEqual(mockError); expect(retry).toHaveBeenCalledTimes(1); - expect(httpGet).toHaveBeenCalledTimes(1); + expect(httpRequest).toHaveBeenCalledTimes(1); }); it("throws Error if requestFromEc2Imds for credentials fails", async () => { const mockError = new Error("creds not found"); - (httpGet as jest.Mock) + (httpRequest as jest.Mock) .mockResolvedValueOnce(mockProfile) .mockRejectedValueOnce(mockError); (retry as jest.Mock).mockImplementation((fn: any) => fn()); await expect(fromInstanceMetadata()()).rejects.toEqual(mockError); expect(retry).toHaveBeenCalledTimes(2); - expect(httpGet).toHaveBeenCalledTimes(2); + expect(httpRequest).toHaveBeenCalledTimes(2); expect(fromImdsCredentials).not.toHaveBeenCalled(); }); it("throws SyntaxError if requestFromEc2Imds returns unparseable creds", async () => { - (httpGet as jest.Mock) + (httpRequest as jest.Mock) .mockResolvedValueOnce(mockProfile) .mockResolvedValueOnce("."); (retry as jest.Mock).mockImplementation((fn: any) => fn()); @@ -169,7 +169,7 @@ describe("fromInstanceMetadata", () => { new SyntaxError("Unexpected token . in JSON at position 0") ); expect(retry).toHaveBeenCalledTimes(2); - expect(httpGet).toHaveBeenCalledTimes(2); + expect(httpRequest).toHaveBeenCalledTimes(2); expect(fromImdsCredentials).not.toHaveBeenCalled(); }); }); diff --git a/packages/credential-provider-imds/src/fromInstanceMetadata.ts b/packages/credential-provider-imds/src/fromInstanceMetadata.ts index 39f5b6f750c7..45eabf71f924 100644 --- a/packages/credential-provider-imds/src/fromInstanceMetadata.ts +++ b/packages/credential-provider-imds/src/fromInstanceMetadata.ts @@ -3,7 +3,7 @@ import { RemoteProviderInit, providerConfigFromInit } from "./remoteProvider/RemoteProviderInit"; -import { httpGet } from "./remoteProvider/httpGet"; +import { httpRequest } from "./remoteProvider/httpRequest"; import { fromImdsCredentials, isImdsCredentials @@ -11,6 +11,9 @@ import { import { retry } from "./remoteProvider/retry"; import { ProviderError } from "@aws-sdk/property-provider"; +const IMDS_IP = "169.254.169.254"; +const IMDS_PATH = "/latest/meta-data/iam/security-credentials/"; + /** * Creates a credential provider that will source credentials from the EC2 * Instance Metadata Service @@ -22,14 +25,23 @@ export const fromInstanceMetadata = ( return async () => { const profile = ( await retry( - async () => await requestFromEc2Imds(timeout), + async () => + ( + await httpRequest({ host: IMDS_IP, path: IMDS_PATH, timeout }) + ).toString(), maxRetries ) ).trim(); return retry(async () => { const credsResponse = JSON.parse( - await requestFromEc2Imds(timeout, profile) + ( + await httpRequest({ + host: IMDS_IP, + path: IMDS_PATH + profile, + timeout + }) + ).toString() ); if (!isImdsCredentials(credsResponse)) { throw new ProviderError( @@ -41,18 +53,3 @@ export const fromInstanceMetadata = ( }, maxRetries); }; }; - -const IMDS_IP = "169.254.169.254"; -const IMDS_PATH = "latest/meta-data/iam/security-credentials"; - -const requestFromEc2Imds = async ( - timeout: number, - path?: string -): Promise => { - const buffer = await httpGet({ - host: IMDS_IP, - path: `/${IMDS_PATH}/${path ? path : ""}`, - timeout - }); - return buffer.toString(); -}; diff --git a/packages/credential-provider-imds/src/remoteProvider/httpGet.spec.ts b/packages/credential-provider-imds/src/remoteProvider/httpGet.spec.ts deleted file mode 100644 index cd4e97fee556..000000000000 --- a/packages/credential-provider-imds/src/remoteProvider/httpGet.spec.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { createServer } from "http"; -import { httpGet } from "./httpGet"; -import { ProviderError } from "@aws-sdk/property-provider"; - -let matchers: { [url: string]: string } = {}; - -function addMatcher(url: string, toReturn: string): void { - matchers[url] = toReturn; -} - -function clearMatchers(): void { - matchers = {}; -} - -function getOpenPort(candidatePort: number = 4321): Promise { - return new Promise((resolve, reject) => { - const server = createServer(); - server.on("error", () => reject()); - server.listen(candidatePort); - server.close(() => resolve(candidatePort)); - }).catch(() => getOpenPort(candidatePort + 1)); -} - -let port: number; - -const server = createServer((request, response) => { - const { url = "" } = request; - if (url in matchers) { - response.statusCode = 200; - response.end(matchers[url]); - } else { - response.statusCode = 404; - response.end("Not found"); - } -}); - -beforeAll(async done => { - port = await getOpenPort(); - server.listen(port); - done(); -}); - -afterAll(() => { - server.close(); -}); - -beforeEach(clearMatchers); - -describe("httpGet", () => { - it("should respond with a promise fulfilled with the http response", async () => { - const expectedResponse = "foo bar baz"; - addMatcher("/", expectedResponse); - - expect( - (await httpGet(`http://localhost:${port}/`)).toString("utf8") - ).toEqual(expectedResponse); - }); - - it("should reject the promise with a non-terminal error if a 404 status code is received", async () => { - addMatcher("/fizz", "buzz"); - - await httpGet(`http://localhost:${port}/foo`).then( - () => { - throw new Error("The promise should have been rejected"); - }, - (err: any) => { - expect((err as ProviderError).tryNextLink).toBe(true); - } - ); - }); - - it("should reject the promise with a non-terminal error if the remote server cannot be contacted", async () => { - server.close(); - - await httpGet(`http://localhost:${port}/foo`).then( - () => { - throw new Error("The promise should have been rejected"); - }, - (err: any) => { - expect((err as ProviderError).tryNextLink).toBe(true); - } - ); - }); -}); diff --git a/packages/credential-provider-imds/src/remoteProvider/httpRequest.spec.ts b/packages/credential-provider-imds/src/remoteProvider/httpRequest.spec.ts new file mode 100644 index 000000000000..a05a8377d70a --- /dev/null +++ b/packages/credential-provider-imds/src/remoteProvider/httpRequest.spec.ts @@ -0,0 +1,95 @@ +import * as nock from "nock"; +import { createServer } from "http"; +import { httpRequest } from "./httpRequest"; +import { ProviderError } from "@aws-sdk/property-provider"; + +describe("httpRequest", () => { + let port: number; + const host = "localhost"; + const path = "/"; + + const getOpenPort = async (candidatePort: number = 4321): Promise => { + try { + return new Promise((resolve, reject) => { + const server = createServer(); + server.on("error", () => reject()); + server.listen(candidatePort); + server.close(() => resolve(candidatePort)); + }); + } catch (e) { + return await getOpenPort(candidatePort + 1); + } + }; + + beforeAll(async () => { + port = await getOpenPort(); + }); + + describe("returns response", () => { + it("defaults to method GET", async () => { + const expectedResponse = "expectedResponse"; + const scope = nock(`http://${host}:${port}`) + .get(path) + .reply(200, expectedResponse); + + const response = await httpRequest({ host, path, port }); + expect(response.toString()).toStrictEqual(expectedResponse); + + scope.done(); + }); + + it("uses method passed in options", async () => { + const method = "POST"; + const expectedResponse = "expectedResponse"; + const scope = nock(`http://${host}:${port}`) + .post(path) + .reply(200, expectedResponse); + + const response = await httpRequest({ host, path, port, method }); + expect(response.toString()).toStrictEqual(expectedResponse); + + scope.done(); + }); + }); + + describe("throws error", () => { + const errorOnStatusCode = async (statusCode: number) => { + it(`statusCode: ${statusCode}`, async () => { + const scope = nock(`http://${host}:${port}`) + .get(path) + .reply(statusCode, "continue"); + + await expect(httpRequest({ host, path, port })).rejects.toStrictEqual( + Object.assign( + new ProviderError( + "Error response received from instance metadata service" + ), + { statusCode } + ) + ); + + scope.done(); + }); + }; + + it("when request throws error", async () => { + const scope = nock(`http://${host}:${port}`) + .get(path) + .replyWithError("error"); + + await expect(httpRequest({ host, path, port })).rejects.toStrictEqual( + new ProviderError("Unable to connect to instance metadata service") + ); + + scope.done(); + }); + + describe("when request returns with statusCode < 200", () => { + [100, 101, 103].forEach(errorOnStatusCode); + }); + + describe("when request returns with statusCode >= 300", () => { + [300, 400, 500].forEach(errorOnStatusCode); + }); + }); +}); diff --git a/packages/credential-provider-imds/src/remoteProvider/httpGet.ts b/packages/credential-provider-imds/src/remoteProvider/httpRequest.ts similarity index 55% rename from packages/credential-provider-imds/src/remoteProvider/httpGet.ts rename to packages/credential-provider-imds/src/remoteProvider/httpRequest.ts index 3413591959ea..4eeed8ad17f0 100644 --- a/packages/credential-provider-imds/src/remoteProvider/httpGet.ts +++ b/packages/credential-provider-imds/src/remoteProvider/httpRequest.ts @@ -1,27 +1,27 @@ import { Buffer } from "buffer"; -import { get, IncomingMessage, RequestOptions } from "http"; +import { request, IncomingMessage, RequestOptions } from "http"; import { ProviderError } from "@aws-sdk/property-provider"; /** * @internal */ -export function httpGet(options: RequestOptions | string): Promise { +export function httpRequest(options: RequestOptions): Promise { return new Promise((resolve, reject) => { - const request = get(options); - request.on("error", err => { + const req = request({ method: "GET", ...options }); + req.on("error", err => { reject( new ProviderError("Unable to connect to instance metadata service") ); }); - request.on("response", (res: IncomingMessage) => { + req.on("response", (res: IncomingMessage) => { const { statusCode = 400 } = res; if (statusCode < 200 || 300 <= statusCode) { - reject( - new ProviderError( - "Error response received from instance metadata service" - ) + const error = new ProviderError( + "Error response received from instance metadata service" ); + (error as any).statusCode = statusCode; + reject(error); } const chunks: Array = []; @@ -32,5 +32,7 @@ export function httpGet(options: RequestOptions | string): Promise { resolve(Buffer.concat(chunks)); }); }); + + req.end(); }); } diff --git a/yarn.lock b/yarn.lock index d586cc6779d0..09debef49dad 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7976,6 +7976,16 @@ no-case@^2.2.0: dependencies: lower-case "^1.1.1" +nock@^13.0.2: + version "13.0.2" + resolved "https://registry.yarnpkg.com/nock/-/nock-13.0.2.tgz#3e50f88348edbb90cce1bbbf0a3ea6a068993983" + integrity sha512-Wm8H22iT3UKPDf138tmgJ0NRfCLd9f2LByki9T2mGHnB66pEqvJh3gV/up1ZufZF24n7/pDYyLGybdqOzF3JIw== + dependencies: + debug "^4.1.0" + json-stringify-safe "^5.0.1" + lodash.set "^4.3.2" + propagate "^2.0.0" + node-fetch-npm@^2.0.2: version "2.0.4" resolved "https://registry.yarnpkg.com/node-fetch-npm/-/node-fetch-npm-2.0.4.tgz#6507d0e17a9ec0be3bec516958a497cec54bf5a4" @@ -8846,6 +8856,11 @@ promzard@^0.3.0: dependencies: read "1" +propagate@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/propagate/-/propagate-2.0.1.tgz#40cdedab18085c792334e64f0ac17256d38f9a45" + integrity sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag== + proto-list@~1.2.1: version "1.2.4" resolved "https://registry.yarnpkg.com/proto-list/-/proto-list-1.2.4.tgz#212d5bfe1318306a420f6402b8e26ff39647a849"