diff --git a/.changeset/spotty-toes-sniff.md b/.changeset/spotty-toes-sniff.md new file mode 100644 index 00000000..b1202b38 --- /dev/null +++ b/.changeset/spotty-toes-sniff.md @@ -0,0 +1,9 @@ +--- +"@siteimprove/alfa-test-utils": minor +--- + +**Added:** New packge for handling test utilities and interaction with Siteimprove Intelligence Platform. + +A new package is now available, intended to wrap several test utilities. It currently contains + +- a wrapper to upload audit results to the Siteimprove Intelligence Platform and see them in the Page Report diff --git a/config/validate-structure.json b/config/validate-structure.json index a3672c12..cfcda044 100644 --- a/config/validate-structure.json +++ b/config/validate-structure.json @@ -29,6 +29,7 @@ "@siteimprove/alfa-puppeteer": ["puppeteer"], "@siteimprove/alfa-react": ["react", "react-test-renderer"], "@siteimprove/alfa-scraper": ["puppeteer"], + "@siteimprove/alfa-test-utils": ["axios"], "@siteimprove/alfa-unexpected": ["unexpected"], "@siteimprove/alfa-vue": [ "@vue/test-utils", diff --git a/docs/review/api/alfa-test-utils.api.md b/docs/review/api/alfa-test-utils.api.md new file mode 100644 index 00000000..33da234e --- /dev/null +++ b/docs/review/api/alfa-test-utils.api.md @@ -0,0 +1,78 @@ +## API Report File for "@siteimprove/alfa-test-utils" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import type { AxiosRequestConfig } from 'axios'; +import type { Flattened } from '@siteimprove/alfa-rules'; +import { Outcome } from '@siteimprove/alfa-act'; +import { Page } from '@siteimprove/alfa-web'; + +// @public +export type alfaOutcome = Outcome; + +// @public +export namespace SIP { + // @internal (undocumented) + export namespace Defaults { + const // (undocumented) + URL = "https://api.siteimprove.com/v2/a11y/AlfaDevCheck/CreateReport"; + const // (undocumented) + Title = "Unnamed page"; + const // (undocumented) + Name = "Accessibility Code Checker"; + } + // @internal + export namespace Metadata { + export function axiosConfig(page: Page, options: Options, override: { + url?: string; + timestamp?: number; + }, defaultTitle?: string, defaultName?: string): AxiosRequestConfig; + export function params(url: string, apiKey: string): AxiosRequestConfig; + // (undocumented) + export interface Payload { + // (undocumented) + PageTitle: string; + // (undocumented) + RequestTimeStampMilliseconds: number; + // (undocumented) + TestName: string; + // (undocumented) + Version: `${number}.${number}.${number}`; + } + export function payload(PageTitle: string, TestName: string, timestamp: number): Payload; + {}; + } + // (undocumented) + export interface Options { + apiKey: string; + pageTitle?: string; + testName?: string; + userName: string; + } + // @internal + export namespace S3 { + export function axiosConfig(id: string, url: string, page: Page, outcomes: Iterable): AxiosRequestConfig; + export function params(url: string): AxiosRequestConfig; + // (undocumented) + export interface Payload { + // (undocumented) + Aspects: string; + // (undocumented) + CheckResult: string; + // (undocumented) + Id: string; + } + export function payload(Id: string, page: Page, outcomes: Iterable): Payload; + {}; + } + export function upload(page: Page, outcomes: Iterable, options: Options): Promise; + // @internal + export function upload(page: Page, outcomes: Iterable, options: Options, override: { + url?: string; + timestamp?: number; + }): Promise; +} + +``` diff --git a/packages/alfa-test-utils/config/api-extractor.json b/packages/alfa-test-utils/config/api-extractor.json new file mode 100644 index 00000000..7c547d46 --- /dev/null +++ b/packages/alfa-test-utils/config/api-extractor.json @@ -0,0 +1,5 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", + "extends": "../../../config/api-extractor.json", + "mainEntryPointFilePath": "/dist/index.d.ts" +} diff --git a/packages/alfa-test-utils/package.json b/packages/alfa-test-utils/package.json new file mode 100644 index 00000000..7d32342b --- /dev/null +++ b/packages/alfa-test-utils/package.json @@ -0,0 +1,44 @@ +{ + "$schema": "http://json.schemastore.org/package", + "name": "@siteimprove/alfa-test-utils", + "homepage": "https://alfa.siteimprove.com", + "version": "0.68.4", + "license": "MIT", + "description": "Utilities to run Alfa tests and upload results to the Siteimprove Intelligence Platform", + "repository": { + "type": "git", + "url": "git+https://github.com/siteimprove/alfa-integrations.git", + "directory": "packages/alfa-test-utils" + }, + "bugs": "https://github.com/siteimprove/alfa/issues", + "engines": { + "node": ">=20.0.0" + }, + "type": "module", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "files": [ + "dist/**/*.js", + "dist/**/*.d.ts" + ], + "publishConfig": { + "access": "public", + "registry": "https://npm.pkg.github.com/" + }, + "dependencies": { + "@siteimprove/alfa-act": "^0.88.0", + "@siteimprove/alfa-dom": "^0.88.0", + "@siteimprove/alfa-json": "^0.88.0", + "@siteimprove/alfa-rules": "^0.88.0", + "@siteimprove/alfa-sequence": "^0.88.0", + "@siteimprove/alfa-web": "^0.88.0", + "axios": "^1.7.2" + }, + "devDependencies": { + "@siteimprove/alfa-device": "^0.88.0", + "@siteimprove/alfa-http": "^0.88.0", + "@siteimprove/alfa-result": "^0.88.0", + "@siteimprove/alfa-test": "^0.88.0", + "axios-mock-adapter": "^1.22.0" + } +} diff --git a/packages/alfa-test-utils/src/common.ts b/packages/alfa-test-utils/src/common.ts new file mode 100644 index 00000000..badad7a3 --- /dev/null +++ b/packages/alfa-test-utils/src/common.ts @@ -0,0 +1,14 @@ +import { Outcome } from "@siteimprove/alfa-act"; +import type { Flattened as Rule } from "@siteimprove/alfa-rules"; + +/** + * The type of individual outcomes produced by Alfa rules. + * + * @public + */ +export type alfaOutcome = Outcome< + Rule.Input, + Rule.Target, + Rule.Question, + Rule.Subject +>; diff --git a/packages/alfa-test-utils/src/index.ts b/packages/alfa-test-utils/src/index.ts new file mode 100644 index 00000000..fd7316e7 --- /dev/null +++ b/packages/alfa-test-utils/src/index.ts @@ -0,0 +1,8 @@ +/** + * A library for running Alfa tests and uploading results to the Siteimprove + * Intelligence Platform. + * + * @packageDocumentation + */ +export * from "./common.js"; +export * from "./report/index.js"; diff --git a/packages/alfa-test-utils/src/report/index.ts b/packages/alfa-test-utils/src/report/index.ts new file mode 100644 index 00000000..fc339f0e --- /dev/null +++ b/packages/alfa-test-utils/src/report/index.ts @@ -0,0 +1 @@ +export * from "./sip.js" diff --git a/packages/alfa-test-utils/src/report/sip.ts b/packages/alfa-test-utils/src/report/sip.ts new file mode 100644 index 00000000..01c66362 --- /dev/null +++ b/packages/alfa-test-utils/src/report/sip.ts @@ -0,0 +1,248 @@ +import { Element, Query } from "@siteimprove/alfa-dom"; +import { Serializable } from "@siteimprove/alfa-json"; +import { alfaVersion } from "@siteimprove/alfa-rules"; +import { Sequence } from "@siteimprove/alfa-sequence"; +import { Page } from "@siteimprove/alfa-web"; + +import type { AxiosRequestConfig } from "axios"; +import axios from "axios"; + +import type { alfaOutcome } from "../common.js"; + +const { Verbosity } = Serializable; + +/** + * Interacting with Siteimprove Intelligence Platform (SIP) API. + * + * @public + */ +export namespace SIP { + /** + * @internal + */ + export namespace Defaults { + export const URL = + "https://api.siteimprove.com/v2/a11y/AlfaDevCheck/CreateReport"; + export const Title = "Unnamed page"; + export const Name = "Accessibility Code Checker"; + } + + /** + * Upload the results of an accessibility check to the Siteimprove Intelligence + * Platform (SIP) API. + * + * @public + */ + export async function upload( + page: Page, + outcomes: Iterable, + options: Options + ): Promise; + + /** + * Internal overload for tests, allowing + * * a custom upload URL (use stage / dev URLs); and + * * mocking timestamp (timestamp stability in tests). + * + * @internal + */ + export async function upload( + page: Page, + outcomes: Iterable, + options: Options, + override: { url?: string; timestamp?: number } + ): Promise; + + export async function upload( + page: Page, + outcomes: Iterable, + options: Options, + override: { url?: string; timestamp?: number } = {} + ): Promise { + const config = Metadata.axiosConfig(page, options, override); + + try { + const axiosResponse = await axios.request(config); + const { pageReportUrl, preSignedUrl, id } = axiosResponse.data; + + const response = await axios.request( + S3.axiosConfig(id, preSignedUrl, page, outcomes) + ); + + return pageReportUrl; + } catch (error) { + console.error(error); + } + + return "Could not retrieve a page report URL"; + } + + /** + * @public + */ + export interface Options { + /** + * The username to connect to the Siteimprove Intelligence Platform + */ + userName: string; + + /** + * The API key to connect to Siteimprove Intelligence Platform + */ + apiKey: string; + + /** + * The title of the page. Defaults to the content of the first `` element, + * if any. + */ + pageTitle?: string; + + /** + * A unique identifier for the test run, e.g. a git commit hash, branch name, … + * + * @remarks + * Unicity is not required but is recommended to help separating unrelated runs. + * Defaults to the generic "Accessibility Code Checker" if none is provided. + */ + testName?: string; + } + + /** + * Handling the metadata request to the Siteimprove API. + * + * @internal + */ + export namespace Metadata { + interface Payload { + RequestTimeStampMilliseconds: number; + Version: `${number}.${number}.${number}`; + PageTitle: string; + TestName: string; + } + + /** + * Prepare payload with metadata for creating pre-signed URL. + */ + export function payload( + PageTitle: string, + TestName: string, + timestamp: number + ): Payload { + return { + RequestTimeStampMilliseconds: timestamp, + Version: alfaVersion, + PageTitle, + TestName, + }; + } + + /** + * Configure parameters of axios request + */ + export function params(url: string, apiKey: string): AxiosRequestConfig { + return { + method: "post", + maxBodyLength: Infinity, + url, + headers: { + "Content-Type": "application/json", + Authorization: "Basic " + Buffer.from(apiKey).toString("base64"), + }, + }; + } + + /** + * Prepare the configuration for the axios request + */ + export function axiosConfig( + page: Page, + options: Options, + override: { url?: string; timestamp?: number }, + defaultTitle = Defaults.Title, + defaultName = Defaults.Name + ): AxiosRequestConfig { + const { url = Defaults.URL, timestamp = Date.now() } = override; + + const title = + options.pageTitle ?? + Query.getElementDescendants(page.document) + .filter(Element.isElement) + .find(Element.hasName("title")) + .map((title) => title.textContent()) + .getOr(defaultTitle); + + const name = options.testName ?? defaultName; + + return { + ...params(url, `${options.userName}:${options.apiKey}`), + data: JSON.stringify(payload(title, name, timestamp)), + }; + } + } + + /** + * Handling the data request to the pre-signed S3 URL. + * + * @internal + */ + export namespace S3 { + interface Payload { + Id: string; + CheckResult: string; + Aspects: string; + } + + /** + * Prepare payload with metadata for creating pre-signed URL. + */ + /** + * Prepare payload with Alfa page and results + * + * @internal + */ + export function payload( + Id: string, + page: Page, + outcomes: Iterable<alfaOutcome> + ): Payload { + return { + Id, + CheckResult: JSON.stringify( + Sequence.from(outcomes).toJSON({ + verbosity: Verbosity.Minimal, + }) + ), + Aspects: JSON.stringify(page.toJSON({ verbosity: Verbosity.High })), + }; + } + + /** + * Configure parameters of axios request + */ + export function params(url: string): AxiosRequestConfig { + return { + method: "put", + maxBodyLength: Infinity, + url, + headers: { "Content-Type": "application/json" }, + }; + } + + /** + * Prepare the configuration for the axios request + */ + export function axiosConfig( + id: string, + url: string, + page: Page, + outcomes: Iterable<alfaOutcome> + ): AxiosRequestConfig { + return { + ...params(url), + data: new Blob([JSON.stringify(payload(id, page, outcomes))], { + type: "application/json", + }), + }; + } + } +} diff --git a/packages/alfa-test-utils/src/tsconfig.json b/packages/alfa-test-utils/src/tsconfig.json new file mode 100644 index 00000000..61d3f60f --- /dev/null +++ b/packages/alfa-test-utils/src/tsconfig.json @@ -0,0 +1,6 @@ +{ + "$schema": "http://json.schemastore.org/tsconfig", + "extends": "../tsconfig.json", + "compilerOptions": { "outDir": "../dist" }, + "files": ["./common.ts", "./index.ts", "report/index.ts", "report/sip.ts"] +} diff --git a/packages/alfa-test-utils/test/sip.spec.tsx b/packages/alfa-test-utils/test/sip.spec.tsx new file mode 100644 index 00000000..52d0951f --- /dev/null +++ b/packages/alfa-test-utils/test/sip.spec.tsx @@ -0,0 +1,225 @@ +import { Diagnostic, Outcome, Rule } from "@siteimprove/alfa-act"; +import { Device } from "@siteimprove/alfa-device"; +import { Document, Element, h } from "@siteimprove/alfa-dom"; +import { Request, Response } from "@siteimprove/alfa-http"; +import { Serializable } from "@siteimprove/alfa-json"; +import { Err } from "@siteimprove/alfa-result"; +import { alfaVersion } from "@siteimprove/alfa-rules"; +import { test } from "@siteimprove/alfa-test"; +import { Page } from "@siteimprove/alfa-web"; + +import { SIP } from "../dist/index.js"; + +import axios from "axios"; +import MockAdapter from "axios-mock-adapter"; + +const { Verbosity } = Serializable; + +const { Metadata, S3 } = SIP; + +const device = Device.standard(); + +function makePage(document: Document): Page { + return Page.of(Request.empty(), Response.empty(), document, device); +} + +test("Metadata.payload() creates a payload", (t) => { + const actual = Metadata.payload("title", "name", 0); + + t.deepEqual(actual, { + RequestTimeStampMilliseconds: 0, + Version: alfaVersion, + PageTitle: "title", + TestName: "name", + }); +}); +test("S3.payload() creates empty-ish payload", (t) => { + const document = h.document([<span></span>]); + + const page = makePage(document); + const actual = S3.payload("some id", page, []); + + t.deepEqual(actual, { + Id: "some id", + CheckResult: "[]", + Aspects: JSON.stringify(page.toJSON({ verbosity: Verbosity.High })), + }); +}); + +test("S3.payload serialises outcomes as string", async (t) => { + const target = <span>Hello</span>; + const document = h.document([target]); + const page = makePage(document); + + const rule = Rule.Atomic.of<Page, Element>({ + uri: "fake rule", + evaluate() { + return { + applicability: () => [target], + expectations: (target) => ({ + 1: Err.of(Diagnostic.of("fake diagnostic")), + }), + }; + }, + }); + + const outcomes = await rule.evaluate(page); + + const actual = S3.payload("some id", page, outcomes); + + const expected: Outcome.Failed.JSON<Element> = { + outcome: Outcome.Value.Failed, + rule: { type: "atomic", uri: "fake rule", requirements: [], tags: [] }, + mode: Outcome.Mode.Automatic, + target: Serializable.toJSON(target, { verbosity: Verbosity.Minimal }), + expectations: [ + ["1", { type: "err", error: { message: "fake diagnostic" } }], + ], + }; + + t.deepEqual(actual, { + Id: "some id", + CheckResult: JSON.stringify([expected]), + Aspects: JSON.stringify(page.toJSON({ verbosity: Verbosity.High })), + }); +}); + +test("Metadata.params() creates axios config", (t) => { + const actual = Metadata.params("url", "key"); + + t.deepEqual(actual, { + method: "post", + maxBodyLength: Infinity, + url: "url", + headers: { + "Content-Type": "application/json", + Authorization: "Basic " + Buffer.from("key").toString("base64"), + }, + }); +}); +test("S3.params() creates axios config", (t) => { + const actual = S3.params("url"); + + t.deepEqual(actual, { + method: "put", + maxBodyLength: Infinity, + url: "url", + headers: { "Content-Type": "application/json" }, + }); +}); + +test("Metadata.axiosConfig() creates an axios config", (t) => { + const page = makePage(h.document([<span></span>])); + + const actual = Metadata.axiosConfig( + page, + { userName: "foo@foo.com", apiKey: "bar" }, + { url: "https://foo.com", timestamp: 0 } + ); + + t.deepEqual(actual, { + ...Metadata.params("https://foo.com", "foo@foo.com:bar"), + data: JSON.stringify( + Metadata.payload(SIP.Defaults.Title, SIP.Defaults.Name, 0) + ), + }); +}); +test("S3.axiosConfig() creates an axios config", (t) => { + const page = makePage(h.document([<span></span>])); + + const actual = S3.axiosConfig("some id", "a pre-signed S3 URL", page, []); + + t.deepEqual(actual, { + ...S3.params("a pre-signed S3 URL"), + data: new Blob([JSON.stringify(S3.payload("some id", page, []))], { + type: "application/json", + }), + }); +}); + +test("Metadata.axiosConfig() uses test name if provided", (t) => { + const page = makePage(h.document([<span></span>])); + + const actual = Metadata.axiosConfig( + page, + { userName: "foo@foo.com", apiKey: "bar", testName: "test name" }, + { url: "https://foo.com", timestamp: 0 } + ); + + t.deepEqual(actual, { + ...Metadata.params("https://foo.com", "foo@foo.com:bar"), + data: JSON.stringify(Metadata.payload(SIP.Defaults.Title, "test name", 0)), + }); +}); + +test("Metadata.axiosConfig() uses explicit title if provided", (t) => { + const page = makePage(h.document([<span></span>])); + + const actual = Metadata.axiosConfig( + page, + { userName: "foo@foo.com", apiKey: "bar", pageTitle: "page title" }, + { url: "https://foo.com", timestamp: 0 } + ); + + t.deepEqual(actual, { + ...Metadata.params("https://foo.com", "foo@foo.com:bar"), + data: JSON.stringify(Metadata.payload("page title", SIP.Defaults.Name, 0)), + }); +}); + +test("Metadata.axiosConfig() uses page's title if it exists", (t) => { + const page = makePage(h.document([<title>Hello, ])); + + const actual = Metadata.axiosConfig( + page, + { userName: "foo@foo.com", apiKey: "bar" }, + { url: "https://foo.com", timestamp: 0 } + ); + + t.deepEqual(actual, { + ...Metadata.params("https://foo.com", "foo@foo.com:bar"), + data: JSON.stringify(Metadata.payload("Hello", SIP.Defaults.Name, 0)), + }); +}); + +test("Metadata.axiosConfig() uses explicit title over page's title", (t) => { + const page = makePage(h.document([ignored, ])); + + const actual = Metadata.axiosConfig( + page, + { userName: "foo@foo.com", apiKey: "bar", pageTitle: "page title" }, + { url: "https://foo.com", timestamp: 0 } + ); + + t.deepEqual(actual, { + ...Metadata.params("https://foo.com", "foo@foo.com:bar"), + data: JSON.stringify(Metadata.payload("page title", SIP.Defaults.Name, 0)), + }); +}); + +// Somehow, importing axios-mock-adapter breaks typing. +// Requiring it is fine, but not allowed in an ESM file. +// @ts-ignore +const mock = new MockAdapter(axios); + +// Everything will be mocked after that, use mock.restore() if needed. +mock + .onPost(SIP.Defaults.URL) + .reply(200, { + pageReportUrl: "a page report URL", + preSignedUrl: "a S3 URL", + id: "hello", + }); + +mock.onPut("a S3 URL").reply(200); + +test(".upload connects to Siteimprove Intelligence Platform", async (t) => { + const page = makePage(h.document([])); + + const actual = await SIP.upload(page, [], { + userName: "foo@foo.com", + apiKey: "bar", + }); + + t.deepEqual(actual, "a page report URL"); +}); diff --git a/packages/alfa-test-utils/test/tsconfig.json b/packages/alfa-test-utils/test/tsconfig.json new file mode 100644 index 00000000..0c0fc127 --- /dev/null +++ b/packages/alfa-test-utils/test/tsconfig.json @@ -0,0 +1,11 @@ +{ + "$schema": "http://json.schemastore.org/tsconfig", + "extends": "../tsconfig.json", + "compilerOptions": { + "outDir": ".", + "jsx": "react-jsx", + "jsxImportSource": "@siteimprove/alfa-dom" + }, + "files": ["./sip.spec.tsx"], + "references": [{ "path": "../src" }] +} diff --git a/packages/alfa-test-utils/tsconfig.json b/packages/alfa-test-utils/tsconfig.json new file mode 100644 index 00000000..80eb6278 --- /dev/null +++ b/packages/alfa-test-utils/tsconfig.json @@ -0,0 +1,5 @@ +{ + "$schema": "http://json.schemastore.org/tsconfig", + "extends": "../tsconfig.json", + "references": [{ "path": "./src" }, { "path": "test" }] +} diff --git a/packages/tsconfig.json b/packages/tsconfig.json index df593b8d..00c504c7 100644 --- a/packages/tsconfig.json +++ b/packages/tsconfig.json @@ -25,6 +25,7 @@ { "path": "alfa-puppeteer" }, { "path": "alfa-react" }, { "path": "alfa-scraper" }, + { "path": "alfa-test-utils" }, { "path": "alfa-unexpected" }, { "path": "alfa-vue" }, { "path": "alfa-webdriver" } diff --git a/yarn.lock b/yarn.lock index 88270e06..367e99fb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3079,6 +3079,25 @@ __metadata: languageName: node linkType: hard +"@siteimprove/alfa-test-utils@workspace:packages/alfa-test-utils": + version: 0.0.0-use.local + resolution: "@siteimprove/alfa-test-utils@workspace:packages/alfa-test-utils" + dependencies: + "@siteimprove/alfa-act": ^0.88.0 + "@siteimprove/alfa-device": ^0.88.0 + "@siteimprove/alfa-dom": ^0.88.0 + "@siteimprove/alfa-http": ^0.88.0 + "@siteimprove/alfa-json": ^0.88.0 + "@siteimprove/alfa-result": ^0.88.0 + "@siteimprove/alfa-rules": ^0.88.0 + "@siteimprove/alfa-sequence": ^0.88.0 + "@siteimprove/alfa-test": ^0.88.0 + "@siteimprove/alfa-web": ^0.88.0 + axios: ^1.7.2 + axios-mock-adapter: ^1.22.0 + languageName: unknown + linkType: soft + "@siteimprove/alfa-test@npm:^0.88.0": version: 0.88.0 resolution: "@siteimprove/alfa-test@npm:0.88.0::__archiveUrl=https%3A%2F%2Fnpm.pkg.github.com%2Fdownload%2F%40Siteimprove%2Falfa-test%2F0.88.0%2Ffa17ef1919586cecc6faaee6124a523772d6d54a" @@ -4483,6 +4502,29 @@ __metadata: languageName: node linkType: hard +"axios-mock-adapter@npm:^1.22.0": + version: 1.22.0 + resolution: "axios-mock-adapter@npm:1.22.0" + dependencies: + fast-deep-equal: ^3.1.3 + is-buffer: ^2.0.5 + peerDependencies: + axios: ">= 0.17.0" + checksum: 3c0b1473a8263958f4409525fc4c9872cc1c055b69b0a9dba84084e8827e22e7b0d0241061b512a67c763f2116dee272761dd5907f235e5337df0d18ead00294 + languageName: node + linkType: hard + +"axios@npm:^1.7.2": + version: 1.7.2 + resolution: "axios@npm:1.7.2" + dependencies: + follow-redirects: ^1.15.6 + form-data: ^4.0.0 + proxy-from-env: ^1.1.0 + checksum: e457e2b0ab748504621f6fa6609074ac08c824bf0881592209dfa15098ece7e88495300e02cd22ba50b3468fd712fe687e629dcb03d6a3f6a51989727405aedf + languageName: node + linkType: hard + "b4a@npm:^1.6.4": version: 1.6.6 resolution: "b4a@npm:1.6.6" @@ -5211,7 +5253,7 @@ __metadata: languageName: node linkType: hard -"combined-stream@npm:^1.0.6, combined-stream@npm:~1.0.6": +"combined-stream@npm:^1.0.6, combined-stream@npm:^1.0.8, combined-stream@npm:~1.0.6": version: 1.0.8 resolution: "combined-stream@npm:1.0.8" dependencies: @@ -6775,6 +6817,16 @@ __metadata: languageName: node linkType: hard +"follow-redirects@npm:^1.15.6": + version: 1.15.6 + resolution: "follow-redirects@npm:1.15.6" + peerDependenciesMeta: + debug: + optional: true + checksum: a62c378dfc8c00f60b9c80cab158ba54e99ba0239a5dd7c81245e5a5b39d10f0c35e249c3379eae719ff0285fff88c365dd446fab19dee771f1d76252df1bbf5 + languageName: node + linkType: hard + "for-each@npm:^0.3.3": version: 0.3.3 resolution: "for-each@npm:0.3.3" @@ -6808,6 +6860,17 @@ __metadata: languageName: node linkType: hard +"form-data@npm:^4.0.0": + version: 4.0.0 + resolution: "form-data@npm:4.0.0" + dependencies: + asynckit: ^0.4.0 + combined-stream: ^1.0.8 + mime-types: ^2.1.12 + checksum: 01135bf8675f9d5c61ff18e2e2932f719ca4de964e3be90ef4c36aacfc7b9cb2fceb5eca0b7e0190e3383fe51c5b37f4cb80b62ca06a99aaabfcfd6ac7c9328c + languageName: node + linkType: hard + "form-data@npm:~2.3.2": version: 2.3.3 resolution: "form-data@npm:2.3.3" @@ -7730,6 +7793,13 @@ fsevents@^2.3.2: languageName: node linkType: hard +"is-buffer@npm:^2.0.5": + version: 2.0.5 + resolution: "is-buffer@npm:2.0.5" + checksum: 764c9ad8b523a9f5a32af29bdf772b08eb48c04d2ad0a7240916ac2688c983bf5f8504bf25b35e66240edeb9d9085461f9b5dae1f3d2861c6b06a65fe983de42 + languageName: node + linkType: hard + "is-callable@npm:^1.1.3, is-callable@npm:^1.1.4, is-callable@npm:^1.1.5, is-callable@npm:^1.2.7": version: 1.2.7 resolution: "is-callable@npm:1.2.7"