diff --git a/components/plainly/actions/create-render-job/create-render-job.mjs b/components/plainly/actions/create-render-job/create-render-job.mjs new file mode 100644 index 0000000000000..3361af8967dd5 --- /dev/null +++ b/components/plainly/actions/create-render-job/create-render-job.mjs @@ -0,0 +1,320 @@ +import plainly from "../../plainly.app.mjs"; +import { parseObjectEntries } from "../../common/utils.mjs"; +import { ConfigurationError } from "@pipedream/platform"; + +export default { + key: "plainly-create-render-job", + name: "Create Render Job", + description: "Creates a render job for a video template. [See the documentation](https://www.plainlyvideos.com/documentation/api-reference)", + version: "0.0.1", + type: "action", + props: { + plainly, + projectId: { + propDefinition: [ + plainly, + "projectId", + ], + }, + templateId: { + propDefinition: [ + plainly, + "templateId", + (c) => ({ + projectId: c.projectId, + }), + ], + }, + attributes: { + type: "object", + label: "Attributes", + description: "User-defined attributes of the render. The field batchRenderId is reserved and should be a string. The field batchRenderSequenceNo is reserved and should be a number. Keys in this map must not contain dots.", + optional: true, + }, + parameters: { + type: "object", + label: "Parameters", + description: "Map of parameters to use in order to resolve the template parametrized layers. Keys in this map must not contain dots.", + optional: true, + }, + configureOptions: { + type: "boolean", + label: "Configure Options", + description: "Set to `true` to enter values for the map of additional options for the render", + optional: true, + reloadProps: true, + }, + captionsPosition: { + type: "string", + label: "Captions Position", + description: "The position of captions. Default: `BOTTOM`", + options: [ + "TOP", + "CENTER", + "BOTTOM", + ], + optional: true, + hidden: true, + }, + captionsStyle: { + type: "string", + label: "Captions Style", + description: "The style of captions. Default: `BASIC`", + options: [ + "BASIC", + "BASIC_WITH_STROKE_AND_SHADOW", + "BASIC_WITH_SHADOW", + "POPPINS_WHITE", + "POPPINS_WHITE_VERTICAL", + ], + optional: true, + hidden: true, + }, + srtFileUrl: { + type: "string", + label: "SRT File URL", + description: "Url to the srt file", + optional: true, + hidden: true, + }, + passthrough: { + type: "string", + label: "Passthrough", + description: "Data to be sent to the integration as the integrationPassthrough parameter. Serves to pass arbitrary data to your active integrations. Could be any string or a render parameter reference in formats {{parameterName}} or {{parameterName:defaultValue}}, including a reference to {{webhookPassthrough}} in the same manner`.", + optional: true, + hidden: true, + }, + skipAll: { + type: "boolean", + label: "Skip All", + description: "If true, any active integration for this project or template will not be triggered.", + optional: true, + hidden: true, + }, + thumbnailAtSeconds: { + type: "string[]", + label: "Thumbnail At Seconds", + description: "Generated thumbnails would be PNG thumbnails based on specified timestamps in seconds", + optional: true, + hidden: true, + }, + thumbnailFormat: { + type: "string", + label: "Thumbnail Format", + description: "The format of the thumbnails", + options: [ + "PNG", + "JPG", + ], + optional: true, + hidden: true, + }, + thumbnailFrequencySeconds: { + type: "integer", + label: "Thumbnail Frequency Seconds", + description: "Frequency in seconds to export a frame. For example, having the value frequencySeconds=5 and a rendered video that lasts 15 sec, would create 3 PNGs.", + optional: true, + hidden: true, + }, + thumbnailFromEncodedVideo: { + type: "boolean", + label: "Thumbnail From Encoded Video", + description: "When set to true, the thumbnails will be generated from the encoded video. If set to false, the thumbnails will be generated from the video outputted by the After Effects rendering process.", + optional: true, + hidden: true, + }, + watermarkEncodingParamsLine: { + type: "string", + label: "Watermark Encodeing Params Line", + description: "Additional ffmpeg command line parameters to change watermark position, size, etc. If not specified, the watermark is placed at the top left corner with opacity of 0.8.", + optional: true, + hidden: true, + }, + watermarkUrl: { + type: "string", + label: "Watermark URL", + description: "A public url to the watermark image or video", + optional: true, + hidden: true, + }, + uploadEnabled: { + type: "boolean", + label: "Upload Enabled", + description: "Enables uploading modified project files as a zip archive", + optional: true, + hidden: true, + }, + configureOutputFormat: { + type: "boolean", + label: "Configure Output Format", + description: "Set to `true` to enter values used for rendering output. If not specified defaults to default output format for the target After Effects version", + optional: true, + reloadProps: true, + }, + attachment: { + type: "boolean", + label: "Attachment", + description: "If the output file should be consider as an attachment, meaning that the video link will initiate a download of the file in a web browser.", + optional: true, + hidden: true, + }, + attachmentFileName: { + type: "string", + label: "Attachment File Name", + description: "Optional, name of the file when downloading as attachment. Note that this has effect only if attachment is true. The file name must be provided without extension which will be added automatically based on the selected format.", + optional: true, + hidden: true, + }, + outputModule: { + type: "string", + label: "Output Module", + description: "The output module defines the format of the video generated by the After Effects", + options: [ + "H_264", + "H_264_HIGH_BIT_RATE", + "HQ", + "HQ_ALPHA", + ], + optional: true, + hidden: true, + }, + settingsTemplate: { + type: "string", + label: "Settings Template", + description: "Defines render settings template to be used during After Effects rendering", + options: [ + "BEST_SETTINGS", + "DRAFT", + ], + optional: true, + hidden: true, + }, + configureWebhook: { + type: "boolean", + label: "Configure Webhook", + description: "Set to `true` to enter webhook properties. A webhook HTTP(S) call expects a 2xx status code in order to be marked as successful. In case of a failed delivery, Plainly will attempt to re-call your webhook for up to one day in space of 15 minutes. A webhook HTTP(S) request has a timeout of 30 seconds. A webhook HTTP(S) request does follow redirects.", + optional: true, + reloadProps: true, + }, + onFailure: { + type: "boolean", + label: "On Failure", + description: "Should webhook be called also on the failed renders", + optional: true, + hidden: true, + }, + onInvalid: { + type: "boolean", + label: "On Invalid", + description: "Should webhook be called also on the invalid renders", + optional: true, + hidden: true, + }, + webhookUrl: { + type: "string", + label: "Webhook URL", + description: "The HTTP(S) webhook URL to execute the POST call once the rendering is finished", + optional: true, + hidden: true, + }, + }, + additionalProps(props) { + // captions options + props.captionsPosition.hidden = !this.configureOptions; + props.captionsStyle.hidden = !this.configureOptions; + props.srtFileUrl.hidden = !this.configureOptions; + + // integrations options + props.passthrough.hidden = !this.configureOptions; + props.skipAll.hidden = !this.configureOptions; + + // project files options + props.uploadEnabled.hidden = !this.configureOptions; + + // thumbnail options + props.thumbnailAtSeconds.hidden = !this.configureOptions; + props.thumbnailFormat.hidden = !this.configureOptions; + props.thumbnailFrequencySeconds.hidden = !this.configureOptions; + props.thumbnailFromEncodedVideo.hidden = !this.configureOptions; + + // watermark options + props.watermarkEncodingParamsLine.hidden = !this.configureOptions; + props.watermarkUrl.hidden = !this.configureOptions; + + // output format config + props.attachment.hidden = !this.configureOutputFormat; + props.attachmentFileName.hidden = !this.configureOutputFormat; + props.outputModule.hidden = !this.configureOutputFormat; + props.settingsTemplate.hidden = !this.configureOutputFormat; + + // webhook config + props.onFailure.hidden = !this.configureWebhook; + props.onInvalid.hidden = !this.configureWebhook; + props.webhookUrl.hidden = !this.configureWebhook; + props.webhookUrl.optional = !this.configureWebhook; + + return {}; + }, + async run({ $ }) { + if ((this.captionsPosition || this.captionsStyle) && !this.srtFileUrl) { + throw new ConfigurationError("SRT File URL is required if setting Captions Position or Style"); + } + + if (this.watermarkEncodingParamsLine && !this.watermarkUrl) { + throw new ConfigurationError("Must specify Watermark URL if specifying Watermark Encoding Params Line"); + } + + const response = await this.plainly.createRender({ + $, + data: { + projectId: this.projectId, + templateId: this.templateId, + attributes: parseObjectEntries(this.attributes), + parameters: parseObjectEntries(this.parameters), + options: { + captions: this.srtFileUrl + ? { + captionsPosition: this.captionsPosition, + captionsStyle: this.captionsStyle, + srtFileUrl: this.srtFileUrl, + } + : undefined, + integration: { + passthrough: this.passthrough, + skipAll: this.skipAll, + }, + projectFiles: { + uploadEnabled: this.uploadEnabled, + }, + thumbnails: { + atSeconds: this.thumbnailAtSeconds, + format: this.thumbnailFormat, + frequencySeconds: this.thumbnailFrequencySeconds, + fromEncodedVideo: this.thumbnailFromEncodedVideo, + }, + watermark: this.watermarkUrl + ? { + encodingParamsLine: this.watermarkEncodingParamsLine, + url: this.watermarkUrl, + } + : undefined, + }, + outputFormat: { + attachment: this.attachment, + attachmentFileName: this.attachmentFileName, + outputModule: this.outputModule, + settingsTemplate: this.settingsTemplate, + }, + webhook: this.webhookUrl + ? { + onFailure: this.onFailure, + onInvalid: this.onInvalid, + url: this.webhookUrl, + } + : undefined, + }, + }); + $.export("$summary", `Successfully created render with ID: ${response.id}`); + return response; + }, +}; diff --git a/components/plainly/actions/get-render-status/get-render-status.mjs b/components/plainly/actions/get-render-status/get-render-status.mjs new file mode 100644 index 0000000000000..9dc54d0a42f5d --- /dev/null +++ b/components/plainly/actions/get-render-status/get-render-status.mjs @@ -0,0 +1,26 @@ +import plainly from "../../plainly.app.mjs"; + +export default { + key: "plainly-get-render-status", + name: "Get Render Status", + description: "Retrieves the current status of a render job in Plainly. [See the documentation](https://www.plainlyvideos.com/documentation/api-reference)", + version: "0.0.1", + type: "action", + props: { + plainly, + renderId: { + propDefinition: [ + plainly, + "renderId", + ], + }, + }, + async run({ $ }) { + const render = await this.plainly.getRender({ + $, + renderId: this.renderId, + }); + $.export("$summary", `Retrieved status ${render.state } for render ${this.renderId}`); + return render; + }, +}; diff --git a/components/plainly/actions/list-templates/list-templates.mjs b/components/plainly/actions/list-templates/list-templates.mjs new file mode 100644 index 0000000000000..e74bbc97dbb8f --- /dev/null +++ b/components/plainly/actions/list-templates/list-templates.mjs @@ -0,0 +1,31 @@ +import plainly from "../../plainly.app.mjs"; + +export default { + key: "plainly-list-templates", + name: "List Templates", + description: "Fetches a list of available video templates in a project in the user's Plainly account. [See the documentation](https://www.plainlyvideos.com/documentation/api-reference)", + version: "0.0.1", + type: "action", + props: { + plainly, + projectId: { + propDefinition: [ + plainly, + "projectId", + ], + }, + }, + async run({ $ }) { + const { templates } = await this.plainly.getProject({ + $, + projectId: this.projectId, + }); + + if (templates?.length) { + $.export("$summary", `Fetched ${templates.length} template${templates.length === 1 + ? "" + : "s"}`); + } + return templates; + }, +}; diff --git a/components/plainly/common/utils.mjs b/components/plainly/common/utils.mjs new file mode 100644 index 0000000000000..37df3ea8be185 --- /dev/null +++ b/components/plainly/common/utils.mjs @@ -0,0 +1,25 @@ +function optionalParseAsJSON(value) { + try { + return JSON.parse(value); + } catch (e) { + return value; + } +} + +export function parseObjectEntries(value) { + if (!value) { + return undefined; + } + const obj = typeof value === "string" + ? JSON.parse(value) + : value; + return Object.fromEntries( + Object.entries(obj).map(([ + key, + value, + ]) => [ + key, + optionalParseAsJSON(value), + ]), + ); +} diff --git a/components/plainly/package.json b/components/plainly/package.json index cef633fe6350e..a8946675ce06d 100644 --- a/components/plainly/package.json +++ b/components/plainly/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/plainly", - "version": "0.0.1", + "version": "0.1.0", "description": "Pipedream Plainly Components", "main": "plainly.app.mjs", "keywords": [ @@ -11,5 +11,8 @@ "author": "Pipedream (https://pipedream.com/)", "publishConfig": { "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" } -} \ No newline at end of file +} diff --git a/components/plainly/plainly.app.mjs b/components/plainly/plainly.app.mjs index 196f356d4dc59..76c7f71cc5554 100644 --- a/components/plainly/plainly.app.mjs +++ b/components/plainly/plainly.app.mjs @@ -1,11 +1,157 @@ +import { axios } from "@pipedream/platform"; + export default { type: "app", app: "plainly", - propDefinitions: {}, + propDefinitions: { + projectId: { + type: "string", + label: "Project ID", + description: "The ID of a project", + async options() { + const projects = await this.listProjects(); + return projects.map((project) => ({ + label: project.name, + value: project.id, + })); + }, + }, + renderId: { + type: "string", + label: "Render ID", + description: "The ID of a render", + async options({ page }) { + const renders = await this.listRenders({ + params: { + page, + }, + }); + return renders.map((render) => ({ + label: `${render.projectName} ${render.templateName} ${render.id}`, + value: render.id, + })); + }, + }, + templateId: { + type: "string", + label: "Template ID", + description: "The ID of the video template", + async options({ projectId }) { + const { templates } = await this.getProject({ + projectId, + }); + return templates.map((template) => ({ + label: template.name, + value: template.id, + })); + }, + }, + brandId: { + type: "string", + label: "Brand ID", + description: "The ID of a brand", + async options() { + const brands = await this.listBrands(); + return brands.map((brand) => ({ + label: brand.settings.name, + value: brand.id, + })); + }, + }, + articleId: { + type: "string", + label: "Article ID", + description: "The ID of an article", + async options({ + brandId, page, + }) { + const articles = await this.listArticles({ + brandId, + params: { + page, + }, + }); + return articles.map((article) => ({ + label: article.input.title, + value: article.id, + })); + }, + }, + }, methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); + _baseUrl() { + return "https://api.plainlyvideos.com/api/v2"; + }, + _makeRequest({ + $ = this, + path, + ...otherOpts + }) { + return axios($, { + url: `${this._baseUrl()}${path}`, + auth: { + username: this.$auth.api_key, + password: "", + }, + ...otherOpts, + }); + }, + getProject({ + projectId, ...opts + }) { + return this._makeRequest({ + path: `/projects/${projectId}`, + ...opts, + }); + }, + getRender({ + renderId, ...opts + }) { + return this._makeRequest({ + path: `/renders/${renderId}`, + ...opts, + }); + }, + listProjects(opts = {}) { + return this._makeRequest({ + path: "/projects", + ...opts, + }); + }, + listRenders(opts = {}) { + return this._makeRequest({ + path: "/renders", + ...opts, + }); + }, + listBrands(opts = {}) { + return this._makeRequest({ + path: "/video-genius/brands", + ...opts, + }); + }, + listArticles({ + brandId, ...opts + }) { + return this._makeRequest({ + path: `/video-genius/brands/${brandId}/articles`, + ...opts, + }); + }, + listVideos({ + brandId, articleId, ...opts + }) { + return this._makeRequest({ + path: `/video-genius/brands/${brandId}/articles/${articleId}/videos`, + ...opts, + }); + }, + createRender(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/renders", + ...opts, + }); }, }, -}; \ No newline at end of file +}; diff --git a/components/plainly/sources/common/base.mjs b/components/plainly/sources/common/base.mjs new file mode 100644 index 0000000000000..fb639c87d70ca --- /dev/null +++ b/components/plainly/sources/common/base.mjs @@ -0,0 +1,98 @@ +import plainly from "../../plainly.app.mjs"; +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; + +export default { + props: { + plainly, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + }, + methods: { + _getLastTs() { + return this.db.get("lastTs") || 0; + }, + _setLastTs(lastTs) { + this.db.set("lastTs", lastTs); + }, + usePagination() { + return true; + }, + getArgs() { + return {}; + }, + getTsField() { + return "createdDate"; + }, + async processEvent(max) { + const lastTs = this._getLastTs(); + let maxTs = lastTs; + + const fn = this.getResourceFn(); + let args = this.getArgs(); + const paginate = this.usePagination(); + const tsField = this.getTsField(); + + if (paginate) { + args = { + ...args, + params: { + ...args?.params, + size: 100, + page: 0, + }, + }; + } + + const results = []; + let total; + do { + const items = await fn(args); + for (const item of items) { + const ts = Date.parse(item[tsField]); + if (ts > lastTs) { + results.push(item); + maxTs = Math.max(ts, maxTs); + } + } + total = items?.length; + if (args.params) { + args.params.page++; + } + } while (paginate && total === args.params.size); + + if (!results.length) { + return; + } + + if (max && results.length > max) { + results.length = max; + } + + results.forEach((item) => { + const meta = this.generateMeta(item); + this.$emit(item, meta); + }); + + this._setLastTs(maxTs); + }, + getResourceFn() { + throw new Error("getResourceFn is not implemented"); + }, + generateMeta() { + throw new Error("generateMeta is not implemented"); + }, + }, + hooks: { + async deploy() { + await this.processEvent(25); + }, + }, + async run() { + await this.processEvent(); + }, +}; diff --git a/components/plainly/sources/new-render-completed/new-render-completed.mjs b/components/plainly/sources/new-render-completed/new-render-completed.mjs new file mode 100644 index 0000000000000..6ec75ad335e9e --- /dev/null +++ b/components/plainly/sources/new-render-completed/new-render-completed.mjs @@ -0,0 +1,56 @@ +import common from "../common/base.mjs"; + +export default { + ...common, + key: "plainly-new-render-completed", + name: "New Render Completed", + description: "Emit new event when a video render job finishes successfully.", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + ...common.props, + projectId: { + propDefinition: [ + common.props.plainly, + "projectId", + ], + optional: true, + }, + templateId: { + propDefinition: [ + common.props.plainly, + "templateId", + (c) => ({ + projectId: c.projectId, + }), + ], + optional: true, + }, + }, + methods: { + ...common.methods, + getResourceFn() { + return this.plainly.listRenders; + }, + getArgs() { + return { + params: { + projectId: this.projectId, + templateId: this.templateId, + state: "DONE", + }, + }; + }, + getTsField() { + return "lastModified"; + }, + generateMeta(item) { + return { + id: item.id, + summary: `New Completed Render with ID: ${item.id}`, + ts: Date.parse(item.lastModified), + }; + }, + }, +}; diff --git a/components/plainly/sources/new-render-failed/new-render-failed.mjs b/components/plainly/sources/new-render-failed/new-render-failed.mjs new file mode 100644 index 0000000000000..9f440b4edec8d --- /dev/null +++ b/components/plainly/sources/new-render-failed/new-render-failed.mjs @@ -0,0 +1,56 @@ +import common from "../common/base.mjs"; + +export default { + ...common, + key: "plainly-new-render-failed", + name: "New Render Failed", + description: "Emit new event when a video render fails.", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + ...common.props, + projectId: { + propDefinition: [ + common.props.plainly, + "projectId", + ], + optional: true, + }, + templateId: { + propDefinition: [ + common.props.plainly, + "templateId", + (c) => ({ + projectId: c.projectId, + }), + ], + optional: true, + }, + }, + methods: { + ...common.methods, + getResourceFn() { + return this.plainly.listRenders; + }, + getArgs() { + return { + params: { + projectId: this.projectId, + templateId: this.templateId, + state: "FAILED", + }, + }; + }, + getTsField() { + return "lastModified"; + }, + generateMeta(item) { + return { + id: item.id, + summary: `New Failed Render with ID: ${item.id}`, + ts: Date.parse(item.lastModified), + }; + }, + }, +}; diff --git a/components/plainly/sources/new-video-created/new-video-created.mjs b/components/plainly/sources/new-video-created/new-video-created.mjs new file mode 100644 index 0000000000000..04689ddc0524a --- /dev/null +++ b/components/plainly/sources/new-video-created/new-video-created.mjs @@ -0,0 +1,51 @@ +import common from "../common/base.mjs"; + +export default { + ...common, + key: "plainly-new-video-created", + name: "New Video Created", + description: "Emit new event when a video is created or uploaded in Plainly.", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + ...common.props, + brandId: { + propDefinition: [ + common.props.plainly, + "brandId", + ], + }, + articleId: { + propDefinition: [ + common.props.plainly, + "articleId", + (c) => ({ + brandId: c.brandId, + }), + ], + }, + }, + methods: { + ...common.methods, + getResourceFn() { + return this.plainly.listVideos; + }, + getArgs() { + return { + brandId: this.brandId, + articleId: this.articleId, + }; + }, + usePagination() { + return false; + }, + generateMeta(item) { + return { + id: item.id, + summary: `New Video Created with ID: ${item.id}`, + ts: Date.parse(item.createdDate), + }; + }, + }, +}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9f137430192b1..60da6a7a774af 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9901,7 +9901,11 @@ importers: specifier: ^1.6.0 version: 1.6.6 - components/plainly: {} + components/plainly: + dependencies: + '@pipedream/platform': + specifier: ^3.0.3 + version: 3.0.3 components/planday: {} @@ -28149,22 +28153,22 @@ packages: superagent@3.8.1: resolution: {integrity: sha512-VMBFLYgFuRdfeNQSMLbxGSLfmXL/xc+OO+BZp41Za/NRDBet/BNbkRJrYzCUu0u4GU0i/ml2dtT8b9qgkw9z6Q==} engines: {node: '>= 4.0'} - deprecated: Please upgrade to v7.0.2+ of superagent. We have fixed numerous issues with streams, form-data, attach(), filesystem errors not bubbling up (ENOENT on attach()), and all tests are now passing. See the releases tab for more information at . + deprecated: Please upgrade to v9.0.0+ as we have fixed a public vulnerability with formidable dependency. Note that v9.0.0+ requires Node.js v14.18.0+. See https://github.com/ladjs/superagent/pull/1800 for insight. This project is supported and maintained by the team at Forward Email @ https://forwardemail.net superagent@4.1.0: resolution: {integrity: sha512-FT3QLMasz0YyCd4uIi5HNe+3t/onxMyEho7C3PSqmti3Twgy2rXT4fmkTz6wRL6bTF4uzPcfkUCa8u4JWHw8Ag==} engines: {node: '>= 6.0'} - deprecated: Please upgrade to v7.0.2+ of superagent. We have fixed numerous issues with streams, form-data, attach(), filesystem errors not bubbling up (ENOENT on attach()), and all tests are now passing. See the releases tab for more information at . + deprecated: Please upgrade to v9.0.0+ as we have fixed a public vulnerability with formidable dependency. Note that v9.0.0+ requires Node.js v14.18.0+. See https://github.com/ladjs/superagent/pull/1800 for insight. This project is supported and maintained by the team at Forward Email @ https://forwardemail.net superagent@5.3.1: resolution: {integrity: sha512-wjJ/MoTid2/RuGCOFtlacyGNxN9QLMgcpYLDQlWFIhhdJ93kNscFonGvrpAHSCVjRVj++DGCglocF7Aej1KHvQ==} engines: {node: '>= 7.0.0'} - deprecated: Please upgrade to v7.0.2+ of superagent. We have fixed numerous issues with streams, form-data, attach(), filesystem errors not bubbling up (ENOENT on attach()), and all tests are now passing. See the releases tab for more information at . + deprecated: Please upgrade to v9.0.0+ as we have fixed a public vulnerability with formidable dependency. Note that v9.0.0+ requires Node.js v14.18.0+. See https://github.com/ladjs/superagent/pull/1800 for insight. This project is supported and maintained by the team at Forward Email @ https://forwardemail.net superagent@7.1.6: resolution: {integrity: sha512-gZkVCQR1gy/oUXr+kxJMLDjla434KmSOKbx5iGD30Ql+AkJQ/YlPKECJy2nhqOsHLjGHzoDTXNSjhnvWhzKk7g==} engines: {node: '>=6.4.0 <13 || >=14'} - deprecated: Please downgrade to v7.1.5 if you need IE/ActiveXObject support OR upgrade to v8.0.0 as we no longer support IE and published an incorrect patch version (see https://github.com/visionmedia/superagent/issues/1731) + deprecated: Please upgrade to v9.0.0+ as we have fixed a public vulnerability with formidable dependency. Note that v9.0.0+ requires Node.js v14.18.0+. See https://github.com/ladjs/superagent/pull/1800 for insight. This project is supported and maintained by the team at Forward Email @ https://forwardemail.net supports-color@2.0.0: resolution: {integrity: sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==}