From 3f89022d425978bf9324a652df02759fb5cc7bbe Mon Sep 17 00:00:00 2001 From: Michael Bromley Date: Thu, 7 May 2020 11:06:23 +0200 Subject: [PATCH] feat(asset-server-plugin): Create S3AssetStorageStrategy Closes #191 --- packages/asset-server-plugin/index.ts | 1 + packages/asset-server-plugin/package.json | 1 + packages/asset-server-plugin/src/plugin.ts | 3 +- .../src/s3-asset-storage-strategy.ts | 232 ++++++++++++++++++ packages/asset-server-plugin/src/types.ts | 4 +- yarn.lock | 117 ++++++++- 6 files changed, 353 insertions(+), 5 deletions(-) create mode 100644 packages/asset-server-plugin/src/s3-asset-storage-strategy.ts diff --git a/packages/asset-server-plugin/index.ts b/packages/asset-server-plugin/index.ts index a3fe81b580..b0d98bed17 100644 --- a/packages/asset-server-plugin/index.ts +++ b/packages/asset-server-plugin/index.ts @@ -1,3 +1,4 @@ export * from './src/plugin'; +export * from './src/s3-asset-storage-strategy'; export * from './src/sharp-asset-preview-strategy'; export * from './src/types'; diff --git a/packages/asset-server-plugin/package.json b/packages/asset-server-plugin/package.json index 7c974dd321..6c98956b2f 100644 --- a/packages/asset-server-plugin/package.json +++ b/packages/asset-server-plugin/package.json @@ -24,6 +24,7 @@ "@types/sharp": "^0.24.0", "@vendure/common": "^0.11.1", "@vendure/core": "^0.11.1", + "aws-sdk": "^2.670.0", "express": "^4.16.4", "node-fetch": "^2.6.0", "rimraf": "^3.0.0", diff --git a/packages/asset-server-plugin/src/plugin.ts b/packages/asset-server-plugin/src/plugin.ts index 3c0b84f09e..cb8b2c86fb 100644 --- a/packages/asset-server-plugin/src/plugin.ts +++ b/packages/asset-server-plugin/src/plugin.ts @@ -27,7 +27,8 @@ import { AssetServerOptions, ImageTransformPreset } from './types'; /** * @description - * The `AssetServerPlugin` serves assets (images and other files) from the local file system. It can also perform on-the-fly image transformations + * The `AssetServerPlugin` serves assets (images and other files) from the local file system, and can also be configured to use + * other storage strategies (e.g. {@link S3AssetStorageStrategy}. It can also perform on-the-fly image transformations * and caches the results for subsequent calls. * * ## Installation diff --git a/packages/asset-server-plugin/src/s3-asset-storage-strategy.ts b/packages/asset-server-plugin/src/s3-asset-storage-strategy.ts new file mode 100644 index 0000000000..f90e39f0cc --- /dev/null +++ b/packages/asset-server-plugin/src/s3-asset-storage-strategy.ts @@ -0,0 +1,232 @@ +import { AssetStorageStrategy, Injector, Logger } from '@vendure/core'; +import { Request } from 'express'; +import { Readable, Stream } from 'stream'; + +import { loggerCtx } from './constants'; +import { AssetServerOptions } from './types'; + +export type S3Credentials = { + accessKeyId: string; + secretAccessKey: string; +}; + +export type S3CredentialsProfile = { + profile: string; +}; + +/** + * @description + * Configuration for connecting to AWS S3. + * + * @docsCategory asset-server-plugin + * @docsPage S3AssetStorageStrategy + */ +export interface S3Config { + /** + * @description + * The credentials used to access your s3 account. You can supply either the access key ID & secret, + * or you can make use of a + * [shared credentials file](https://docs.aws.amazon.com/sdk-for-javascript/v2/developer-guide/loading-node-credentials-shared.html) + * and supply the profile name (e.g. `'default'`) + */ + credentials: S3Credentials | S3CredentialsProfile; + /** + * @description + * The S3 bucket in which to store the assets. If it does not exist, it will be created on startup. + */ + bucket: string; + /** + * @description + * The AWS region in which to host the assets. + */ + region?: string; +} + +/** + * @description + * Returns a configured instance of the {@link S3AssetStorageStrategy} which can then be passed to the {@link AssetServerOptions} + * `storageStrategyFactory` property. + * + * Before using this strategy, make sure you have the `aws-sdk` package installed: + * + * ```sh + * npm install aws-sdk + * ``` + * + * @example + * ```TypeScript + * plugins: [ + * AssetServerPlugin.init({ + * route: 'assets', + * assetUploadDir: path.join(__dirname, 'assets'), + * port: 5002, + * namingStrategy: new DefaultAssetNamingStrategy(), + * storageStrategyFactory: configureS3AssetStorage({ + * bucket: 'my-s3-bucket', + * credentials: { + * accessKeyId: process.env.AWS_ACCESS_KEY_ID, + * secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY, + * }, + * }), + * }), + * ``` + * + * @docsCategory asset-server-plugin + * @docsPage S3AssetStorageStrategy + */ +export function configureS3AssetStorage(s3Config: S3Config) { + return (options: AssetServerOptions) => { + const { assetUrlPrefix, route } = options; + const toAbsoluteUrlFn = (request: Request, identifier: string): string => { + if (!identifier) { + return ''; + } + const prefix = assetUrlPrefix || `${request.protocol}://${request.get('host')}/${route}/`; + return identifier.startsWith(prefix) ? identifier : `${prefix}${identifier}`; + }; + return new S3AssetStorageStrategy(s3Config, toAbsoluteUrlFn); + }; +} + +/** + * @description + * An {@link AssetStorageStrategy} which uses [Amazon S3](https://aws.amazon.com/s3/) object storage service. + * To us this strategy you must first have access to an AWS account. + * See their [getting started guide](https://aws.amazon.com/s3/getting-started/?nc=sn&loc=5) for how to get set up. + * + * Before using this strategy, make sure you have the `aws-sdk` package installed: + * + * ```sh + * npm install aws-sdk + * ``` + * + * **Note:** Rather than instantiating this manually, use the {@link configureS3AssetStorage} function. + * + * @docsCategory asset-server-plugin + * @docsPage S3AssetStorageStrategy + */ +export class S3AssetStorageStrategy implements AssetStorageStrategy { + private AWS: typeof import('aws-sdk'); + private s3: import('aws-sdk').S3; + constructor( + private s3Config: S3Config, + public readonly toAbsoluteUrl: (reqest: Request, identifier: string) => string, + ) {} + + async init() { + try { + this.AWS = await import('aws-sdk'); + } catch (e) { + Logger.error( + `Could not find the "aws-sdk" package. Make sure it is installed`, + loggerCtx, + e.stack, + ); + } + + this.setCredentials(); + if (this.s3Config.region) { + this.AWS.config.update({ region: this.s3Config.region }); + } + this.s3 = new this.AWS.S3(); + await this.ensureBucket(this.s3Config.bucket); + } + + destroy?: (() => void | Promise) | undefined; + + async writeFileFromBuffer(fileName: string, data: Buffer): Promise { + const result = await this.s3 + .upload({ + Bucket: this.s3Config.bucket, + Key: fileName, + Body: data, + }) + .promise(); + return result.Key; + } + + async writeFileFromStream(fileName: string, data: Stream): Promise { + const result = await this.s3 + .upload({ + Bucket: this.s3Config.bucket, + Key: fileName, + Body: data, + }) + .promise(); + return result.Key; + } + + async readFileToBuffer(identifier: string): Promise { + const result = await this.s3.getObject(this.getObjectParams(identifier)).promise(); + return Buffer.from(result.Body as Stream); + } + + async readFileToStream(identifier: string): Promise { + const result = await this.s3.getObject(this.getObjectParams(identifier)).promise(); + const body = result.Body; + if (!(body instanceof Stream)) { + const readable = new Readable(); + readable._read = () => { + /* noop */ + }; + readable.push(body); + readable.push(null); + return readable; + } + return body; + } + + async deleteFile(identifier: string): Promise { + await this.s3.deleteObject(this.getObjectParams(identifier)).promise(); + } + + async fileExists(fileName: string): Promise { + try { + await this.s3.headObject(this.getObjectParams(fileName)).promise(); + return true; + } catch (e) { + return false; + } + } + + private getObjectParams(identifier: string) { + return { + Bucket: this.s3Config.bucket, + Key: identifier.replace(/^\//, '').replace(/\//g, '\\'), + }; + } + + private setCredentials() { + const { credentials } = this.s3Config; + if (this.isCredentialsProfile(credentials)) { + this.AWS.config.credentials = new this.AWS.SharedIniFileCredentials(credentials); + } else { + this.AWS.config.credentials = new this.AWS.Credentials(credentials); + } + } + + private async ensureBucket(bucket: string) { + let bucketExists = false; + try { + await this.s3.headBucket({ Bucket: this.s3Config.bucket }).promise(); + bucketExists = true; + Logger.verbose(`Found S3 bucket "${bucket}"`, loggerCtx); + } catch (e) { + Logger.verbose(`Could not find bucket "${bucket}". Attempting to create...`); + } + if (!bucketExists) { + try { + await this.s3.createBucket({ Bucket: bucket, ACL: 'private' }).promise(); + Logger.verbose(`Created S3 bucket "${bucket}"`, loggerCtx); + } catch (e) { + Logger.error(`Could not find nor create the S3 bucket "${bucket}"`, loggerCtx, e.stack); + } + } + } + + private isCredentialsProfile( + credentials: S3Credentials | S3CredentialsProfile, + ): credentials is S3CredentialsProfile { + return credentials.hasOwnProperty('profile'); + } +} diff --git a/packages/asset-server-plugin/src/types.ts b/packages/asset-server-plugin/src/types.ts index bc82777fb7..4c083ef77a 100644 --- a/packages/asset-server-plugin/src/types.ts +++ b/packages/asset-server-plugin/src/types.ts @@ -59,9 +59,9 @@ export interface AssetServerOptions { route: string; /** * @description - * The local directory to which assets will be uploaded. + * The local directory to which assets will be uploaded when using the {@link LocalAssetStorageStrategy}. */ - assetUploadDir: string; + assetUploadDir: string; // TODO: this is strategy-specific and should be moved out of the global options /** * @description * The complete URL prefix of the asset files. For example, "https://demo.vendure.io/assets/" diff --git a/yarn.lock b/yarn.lock index e806aa7c4b..9db6b244f6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2769,7 +2769,7 @@ multer "1.4.2" tslib "1.11.1" -"@nestjs/terminus@^7.0.1": +"@nestjs/terminus@7.0.1": version "7.0.1" resolved "https://registry.npmjs.org/@nestjs/terminus/-/terminus-7.0.1.tgz#7d748f8c18973d60023a8ab16760d0adab145b8b" integrity sha512-OKg1QQDb+whHJM3Xt+3RRUPiyZSyD0qLacfldK0TXcFpKyexA0yyY3GKeaBNApf01FEzJgkK3ARCUoELnAfXDA== @@ -3138,6 +3138,11 @@ dependencies: defer-to-connect "^1.0.1" +"@tokenizer/token@^0.1.0", "@tokenizer/token@^0.1.1": + version "0.1.1" + resolved "https://registry.npmjs.org/@tokenizer/token/-/token-0.1.1.tgz#f0d92c12f87079ddfd1b29f614758b9696bc29e3" + integrity sha512-XO6INPbZCxdprl+9qa/AAbFFOMzzwqYxpjPgLICrMD6C2FCw6qfJOPcBk6JqqPLSaZ/Qx87qn4rpPmPMwaAK6w== + "@tootallnate/once@1": version "1.0.0" resolved "https://registry.npmjs.org/@tootallnate/once/-/once-1.0.0.tgz#9c13c2574c92d4503b005feca8f2e16cc1611506" @@ -3276,6 +3281,11 @@ resolved "https://registry.npmjs.org/@types/debounce/-/debounce-1.2.0.tgz#9ee99259f41018c640b3929e1bb32c3dcecdb192" integrity sha512-bWG5wapaWgbss9E238T0R6bfo5Fh3OkeoSt245CM7JJwVwpw6MEBCbIxLq5z8KzsE3uJhzcIuQkyiZmzV3M/Dw== +"@types/debug@^4.1.5": + version "4.1.5" + resolved "https://registry.npmjs.org/@types/debug/-/debug-4.1.5.tgz#b14efa8852b7768d898906613c23f688713e02cd" + integrity sha512-Q1y515GcOdTHgagaVFhHnIFQ38ygs/kmxdNpvpou+raI9UO3YZcHDngBSYKQklcKlvA7iuQlmIKbzvmxcOE9CQ== + "@types/detect-port@^1.1.0": version "1.1.0" resolved "https://registry.npmjs.org/@types/detect-port/-/detect-port-1.1.0.tgz#07075d264e2e5a432624b1e7ffc11379fe66be8a" @@ -4955,6 +4965,21 @@ await-to-js@^2.0.1: resolved "https://registry.npmjs.org/await-to-js/-/await-to-js-2.1.1.tgz#c2093cd5a386f2bb945d79b292817bbc3f41b31b" integrity sha512-CHBC6gQGCIzjZ09tJ+XmpQoZOn4GdWePB4qUweCaKNJ0D3f115YdhmYVTZ4rMVpiJ3cFzZcTYK1VMYEICV4YXw== +aws-sdk@^2.670.0: + version "2.670.0" + resolved "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.670.0.tgz#d54d18b9245df7b89bea96102e5bdebd99587701" + integrity sha512-hGRnZtp1wDUh6hZRBHO0Ki7thx/xbRlIEiTKlWes+f/0E1Nhm3KpelsBZ3L/Q6y1ragwkQd4Q720AmWEqemLyA== + dependencies: + buffer "4.9.1" + events "1.1.1" + ieee754 "1.1.13" + jmespath "0.15.0" + querystring "0.2.0" + sax "1.2.1" + url "0.10.3" + uuid "3.3.2" + xml2js "0.4.19" + aws-sign2@~0.7.0: version "0.7.0" resolved "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" @@ -5477,6 +5502,15 @@ buffer-xor@^1.0.3: resolved "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz#26e61ed1422fb70dd42e6e36729ed51d855fe8d9" integrity sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk= +buffer@4.9.1: + version "4.9.1" + resolved "https://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz#6d1bb601b07a4efced97094132093027c95bc298" + integrity sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg= + dependencies: + base64-js "^1.0.2" + ieee754 "^1.1.4" + isarray "^1.0.0" + buffer@^4.3.0: version "4.9.2" resolved "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz#230ead344002988644841ab0244af8c44bbe3ef8" @@ -7990,6 +8024,11 @@ eventemitter3@^4.0.0: resolved "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.0.tgz#d65176163887ee59f386d64c82610b696a4a74eb" integrity sha512-qerSRB0p+UDEssxTtm6EDKcE7W4OaoisfIMl4CngyEhjpYglocpNg6UEqCvemdGhosAsg4sO2dXJOdyBifPGCg== +events@1.1.1: + version "1.1.1" + resolved "https://registry.npmjs.org/events/-/events-1.1.1.tgz#9ebdb7635ad099c70dcc4c2a1f5004288e8bd924" + integrity sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ= + events@^3.0.0: version "3.1.0" resolved "https://registry.npmjs.org/events/-/events-3.1.0.tgz#84279af1b34cb75aa88bf5ff291f6d0bd9b31a59" @@ -8375,6 +8414,16 @@ file-loader@4.2.0: loader-utils "^1.2.3" schema-utils "^2.0.0" +file-type@^14.3.0: + version "14.3.0" + resolved "https://registry.npmjs.org/file-type/-/file-type-14.3.0.tgz#0afc57210e3c655d2106a2eba026d3d5161fea79" + integrity sha512-s71v6jMkbfwVdj87csLeNpL5K93mv4lN+lzgzifoICtPHhnXokDwBa3jrzfg+z6FK872iYJ0vS0i74v8XmoFDA== + dependencies: + readable-web-to-node-stream "^2.0.0" + strtok3 "^6.0.0" + token-types "^2.0.0" + typedarray-to-buffer "^3.1.5" + file-uri-to-path@1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd" @@ -9722,7 +9771,7 @@ iconv-lite@0.4.24, iconv-lite@^0.4.24, iconv-lite@^0.4.4, iconv-lite@~0.4.13: dependencies: safer-buffer ">= 2.1.2 < 3" -ieee754@^1.1.4: +ieee754@1.1.13, ieee754@^1.1.13, ieee754@^1.1.4: version "1.1.13" resolved "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz#ec168558e95aa181fd87d37f55c32bbcb6708b84" integrity sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg== @@ -11063,6 +11112,11 @@ jest@^25.2.1: import-local "^3.0.2" jest-cli "^25.2.1" +jmespath@0.15.0: + version "0.15.0" + resolved "https://registry.npmjs.org/jmespath/-/jmespath-0.15.0.tgz#a3f222a9aae9f966f5d27c796510e28091764217" + integrity sha1-o/Iiqarp+Wb10nx5ZRDigJF2Qhc= + js-beautify@^1.6.14: version "1.10.3" resolved "https://registry.npmjs.org/js-beautify/-/js-beautify-1.10.3.tgz#c73fa10cf69d3dfa52d8ed624f23c64c0a6a94c1" @@ -14450,6 +14504,11 @@ pbkdf2@^3.0.3: safe-buffer "^5.0.1" sha.js "^2.4.8" +peek-readable@^3.1.0: + version "3.1.0" + resolved "https://registry.npmjs.org/peek-readable/-/peek-readable-3.1.0.tgz#250b08b7de09db8573d7fd8ea475215bbff14348" + integrity sha512-KGuODSTV6hcgdZvDrIDBUkN0utcAVj1LL7FfGbM0viKTtCHmtZcuEJ+lGqsp0fTFkGqesdtemV2yUSMeyy3ddA== + pend@~1.2.0: version "1.2.0" resolved "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50" @@ -15618,6 +15677,11 @@ readable-stream@1.1.x: string_decoder "^1.1.1" util-deprecate "^1.0.1" +readable-web-to-node-stream@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/readable-web-to-node-stream/-/readable-web-to-node-stream-2.0.0.tgz#751e632f466552ac0d5c440cc01470352f93c4b7" + integrity sha512-+oZJurc4hXpaaqsN68GoZGQAQIA3qr09Or4fqEsargABnbe5Aau8hFn6ISVleT3cpY/0n/8drn7huyyEvTbghA== + readdir-scoped-modules@^1.0.0, readdir-scoped-modules@^1.1.0: version "1.1.0" resolved "https://registry.npmjs.org/readdir-scoped-modules/-/readdir-scoped-modules-1.1.0.tgz#8d45407b4f870a0dcaebc0e28670d18e74514309" @@ -16261,6 +16325,11 @@ saucelabs@^1.5.0: dependencies: https-proxy-agent "^2.2.1" +sax@1.2.1: + version "1.2.1" + resolved "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz#7b8e656190b228e81a66aea748480d828cd2d37a" + integrity sha1-e45lYZCyKOgaZq6nSEgNgozS03o= + sax@>=0.6.0, sax@^1.2.4, sax@~1.2.4: version "1.2.4" resolved "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" @@ -17234,6 +17303,16 @@ strong-log-transformer@^2.0.0: minimist "^1.2.0" through "^2.3.4" +strtok3@^6.0.0: + version "6.0.0" + resolved "https://registry.npmjs.org/strtok3/-/strtok3-6.0.0.tgz#d6b900863daeacfe6c1724c6e7bb36d7a58e83c8" + integrity sha512-ZXlmE22LZnIBvEU3n/kZGdh770fYFie65u5+2hLK9s74DoFtpkQIdBZVeYEzlolpGa+52G5IkzjUWn+iXynOEQ== + dependencies: + "@tokenizer/token" "^0.1.1" + "@types/debug" "^4.1.5" + debug "^4.1.1" + peek-readable "^3.1.0" + style-loader@1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/style-loader/-/style-loader-1.0.0.tgz#1d5296f9165e8e2c85d24eee0b7caf9ec8ca1f82" @@ -17675,6 +17754,14 @@ toidentifier@1.0.0: resolved "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553" integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw== +token-types@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/token-types/-/token-types-2.0.0.tgz#b23618af744818299c6fbf125e0fdad98bab7e85" + integrity sha512-WWvu8sGK8/ZmGusekZJJ5NM6rRVTTDO7/bahz4NGiSDb/XsmdYBn6a1N/bymUHuWYTWeuLUg98wUzvE4jPdCZw== + dependencies: + "@tokenizer/token" "^0.1.0" + ieee754 "^1.1.13" + tough-cookie@^2.3.3, tough-cookie@~2.5.0: version "2.5.0" resolved "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2" @@ -18196,6 +18283,14 @@ url-parse@^1.4.3: querystringify "^2.1.1" requires-port "^1.0.0" +url@0.10.3: + version "0.10.3" + resolved "https://registry.npmjs.org/url/-/url-0.10.3.tgz#021e4d9c7705f21bbf37d03ceb58767402774c64" + integrity sha1-Ah5NnHcF8hu/N9A861h2dAJ3TGQ= + dependencies: + punycode "1.3.2" + querystring "0.2.0" + url@^0.11.0: version "0.11.0" resolved "https://registry.npmjs.org/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1" @@ -18265,6 +18360,11 @@ utils-merge@1.0.1: resolved "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM= +uuid@3.3.2: + version "3.3.2" + resolved "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131" + integrity sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA== + uuid@7.0.2, uuid@^7.0.1: version "7.0.2" resolved "https://registry.npmjs.org/uuid/-/uuid-7.0.2.tgz#7ff5c203467e91f5e0d85cfcbaaf7d2ebbca9be6" @@ -18911,6 +19011,14 @@ xml-name-validator@^3.0.0: resolved "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a" integrity sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw== +xml2js@0.4.19: + version "0.4.19" + resolved "https://registry.npmjs.org/xml2js/-/xml2js-0.4.19.tgz#686c20f213209e94abf0d1bcf1efaa291c7827a7" + integrity sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q== + dependencies: + sax ">=0.6.0" + xmlbuilder "~9.0.1" + xml2js@^0.4.17: version "0.4.23" resolved "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz#a0c69516752421eb2ac758ee4d4ccf58843eac66" @@ -18924,6 +19032,11 @@ xmlbuilder@~11.0.0: resolved "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz#be9bae1c8a046e76b31127726347d0ad7002beb3" integrity sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA== +xmlbuilder@~9.0.1: + version "9.0.7" + resolved "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz#132ee63d2ec5565c557e20f4c22df9aca686b10d" + integrity sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0= + xmlchars@^2.1.1: version "2.2.0" resolved "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb"