├── .gitignore ├── README.md ├── admin └── src │ ├── components │ ├── Initializer │ │ └── index.js │ ├── Input │ │ └── index.js │ └── PluginIcon │ │ └── index.js │ ├── index.js │ ├── pages │ ├── App │ │ └── index.js │ └── HomePage │ │ └── index.js │ ├── pluginId.js │ ├── translations │ ├── en.json │ └── fr.json │ └── utils │ ├── axiosInstance.js │ └── getTrad.js ├── package.json ├── server ├── bootstrap.js ├── config │ └── index.js ├── content-types │ └── index.js ├── controllers │ ├── ai-controller.js │ └── index.js ├── destroy.js ├── index.js ├── middlewares │ └── index.js ├── policies │ └── index.js ├── register.js ├── routes │ └── index.js └── services │ ├── index.js │ └── open-ai.js ├── strapi-admin.js ├── strapi-server.js └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | ############################ 2 | # OS X 3 | ############################ 4 | 5 | .DS_Store 6 | .AppleDouble 7 | .LSOverride 8 | Icon 9 | .Spotlight-V100 10 | .Trashes 11 | ._* 12 | 13 | 14 | ############################ 15 | # Linux 16 | ############################ 17 | 18 | *~ 19 | 20 | 21 | ############################ 22 | # Windows 23 | ############################ 24 | 25 | Thumbs.db 26 | ehthumbs.db 27 | Desktop.ini 28 | $RECYCLE.BIN/ 29 | *.cab 30 | *.msi 31 | *.msm 32 | *.msp 33 | 34 | 35 | ############################ 36 | # Packages 37 | ############################ 38 | 39 | *.7z 40 | *.csv 41 | *.dat 42 | *.dmg 43 | *.gz 44 | *.iso 45 | *.jar 46 | *.rar 47 | *.tar 48 | *.zip 49 | *.com 50 | *.class 51 | *.dll 52 | *.exe 53 | *.o 54 | *.seed 55 | *.so 56 | *.swo 57 | *.swp 58 | *.swn 59 | *.swm 60 | *.out 61 | *.pid 62 | 63 | 64 | ############################ 65 | # Logs and databases 66 | ############################ 67 | 68 | .tmp 69 | *.log 70 | *.sql 71 | *.sqlite 72 | *.sqlite3 73 | 74 | 75 | ############################ 76 | # Misc. 77 | ############################ 78 | 79 | *# 80 | ssl 81 | .idea 82 | nbproject 83 | public/uploads/* 84 | !public/uploads/.gitkeep 85 | 86 | ############################ 87 | # Node.js 88 | ############################ 89 | 90 | lib-cov 91 | lcov.info 92 | pids 93 | logs 94 | results 95 | node_modules 96 | .node_history 97 | 98 | ############################ 99 | # Tests 100 | ############################ 101 | 102 | testApp 103 | coverage 104 | 105 | ############################ 106 | # Strapi 107 | ############################ 108 | 109 | .env 110 | license.txt 111 | exports 112 | *.cache 113 | dist 114 | build 115 | .strapi-updater.json 116 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Strapi AI Text Generation Custom Feild with Open AI 2 | 3 | With this Custom Field, you can leverage Open AI to generate CMS content for your products, webistes, blogs or whatever your heat desires. 4 | 5 | 6 | A Custom Field called writer that generates text for content editors. 7 | 8 | 9 | ### ✨ Coming Soon 10 | - Change the number of words the API generates 11 | - Change the langauge model used for text generation 12 | 13 | ## 🔧 Installation 14 | 15 | Inside your Strapi Application, add the package 16 | 17 | With `npm` 18 | 19 | ```bash 20 | npm install ai-text-generation 21 | ``` 22 | 23 | or `yarn` 24 | 25 | ```bash 26 | yarn add ai-text-generation 27 | ``` 28 | 29 | 30 | Then get your Open AI API Token [here](https://beta.openai.com/account/api-keys) and add it to the `.env` file at the root of your project as `OPEN_AI_API_TOKEN`. Next, in `./config`, create a file called `plugins.js` to enable the plugin. 31 | 32 | ```javascript 33 | // ... 34 | 'ai-text-generation': { 35 | enabled: true, 36 | config: { 37 | apiToken: process.env.OPEN_AI_API_TOKEN, 38 | }, 39 | }, 40 | //... 41 | ``` 42 | 43 | 44 | After, build your admin with 45 | 46 | ```bash 47 | npm run build 48 | ``` 49 | 50 | or 51 | 52 | ```bash 53 | yarn build 54 | ``` 55 | 56 | and start your server with 57 | 58 | ```bash 59 | npm run develop 60 | ``` 61 | 62 | or 63 | 64 | ```bash 65 | yarn develop 66 | ``` 67 | 68 | and now you have the power of Open AI, right in your favourite CMS. 69 | For more on how to enable Custom Feilds in your content Model, have a look at this video on ["How to install and use Strapi custom fields"](https://www.youtube.com/watch?v=hIKfvLzN6VI). 70 | 71 | ## 🛠 Contributing 72 | 73 | In this section, we'll look at how YOU can contribute to this Custom Field. 74 | 75 | ### Setting up the environment 76 | 77 | Start by creating a new Strapi project. 78 | 79 | ```bash 80 | npx create-strapi-app --quickstart strapi-plugin-dev 81 | cd strapi-plugin-dev 82 | ``` 83 | 84 | Create a new plugins folder if it doesn't exist already. 85 | ```bash 86 | mkdir -p src/plugins 87 | ``` 88 | 89 | Now we should clone this repository so you can work on it. 90 | 91 | ```bash 92 | git clone https://github.com/malgamves/strapi-open-ai-text-generation.git src/plugins/ai-text-generation 93 | ``` 94 | 95 | Install project dependencies 96 | 97 | ```bash 98 | yarn install 99 | ``` 100 | 101 | Now we need to register plugin so strapi can use it. In order to do that we need to create (if not already created) `./config/plugins.js` file and add entry to it. 102 | 103 | ```javascript 104 | // ... 105 | 'ai-text-generation': { 106 | enabled: true, 107 | config: { 108 | apiToken: process.env.OPEN_AI_API_TOKEN, 109 | }, 110 | resolve: './src/plugins/ai-text-generation' 111 | }, 112 | // ... 113 | ``` 114 | 115 | > When contributing, you need to change the value of the `fetch()` url below https://github.com/malgamves/strapi-open-ai-text-generation/blob/78e692b214fc51f8de6bbf6a76fca4db767411eb/admin/src/components/Input/index.js#L25 to `http://localhost:1337` 116 | 117 | Rebuild the project and start the server 118 | 119 | ```bash 120 | yarn build 121 | yarn develop 122 | ``` 123 | 124 | For an optimum development experience, start your Strapi server in `Watch Mode` so you don't have to build everytime you make changes as show below 125 | ```bash 126 | yarn develop --watch-admin 127 | ``` 128 | 129 | Thanks to the team at [CKEditor](https://ckeditor.com/), the format of this Readme is [inspired by theirs](https://github.com/ckeditor/strapi-plugin-ckeditor). 130 | -------------------------------------------------------------------------------- /admin/src/components/Initializer/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Initializer 4 | * 5 | */ 6 | 7 | import { useEffect, useRef } from 'react'; 8 | import PropTypes from 'prop-types'; 9 | import pluginId from '../../pluginId'; 10 | 11 | const Initializer = ({ setPlugin }) => { 12 | const ref = useRef(); 13 | ref.current = setPlugin; 14 | 15 | useEffect(() => { 16 | ref.current(pluginId); 17 | }, []); 18 | 19 | return null; 20 | }; 21 | 22 | Initializer.propTypes = { 23 | setPlugin: PropTypes.func.isRequired, 24 | }; 25 | 26 | export default Initializer; 27 | -------------------------------------------------------------------------------- /admin/src/components/Input/index.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { useIntl } from 'react-intl'; 3 | import { TextInput } from '@strapi/design-system/TextInput'; 4 | import { Stack } from '@strapi/design-system/Stack'; 5 | import { Button } from '@strapi/design-system/Button'; 6 | import { Textarea } from '@strapi/design-system'; 7 | import { auth } from '@strapi/helper-plugin' 8 | 9 | 10 | export default function Index({ 11 | name, 12 | error, 13 | description, 14 | onChange, 15 | value, 16 | intlLabel, 17 | attribute, 18 | }) { 19 | const { formatMessage } = useIntl(); 20 | const [prompt, setPrompt] = useState(''); 21 | const [err, setErr] = useState(''); 22 | 23 | const generateText = async () => { 24 | try { 25 | const response = await fetch(`/ai-text-generation/generate-text`, { 26 | method: 'POST', 27 | headers: { 28 | 'Content-Type': 'application/json', 29 | 'Authorization': `Bearer ${auth.getToken()}` 30 | }, 31 | body: JSON.stringify({ 32 | 'model': 'text-davinci-001', 33 | 'prompt': `${prompt}`, 34 | 'temperature': 0.4, 35 | 'max_tokens': parseFloat(attribute.options.length), 36 | 'top_p': 1, 37 | 'frequency_penalty': 0, 38 | 'presence_penalty': 0 39 | }) 40 | }); 41 | 42 | if (!response.ok) { 43 | throw new Error(`Error! status: ${response.status}`); 44 | } 45 | 46 | const result = await response.json(); 47 | const parsedResult = result.choices[0].text.replace(/(?:\r\n|\r|\n)/g, ''); 48 | 49 | onChange({ target: { name, value: parsedResult, type: attribute.type } }) 50 | } catch (err) { 51 | setErr(err.message); 52 | } 53 | } 54 | 55 | const clearGeneratedText = async () => { 56 | onChange({ target: { name, value: '', type: attribute.type } }) 57 | 58 | } 59 | 60 | return ( 61 | 62 | setPrompt(e.target.value)} 67 | value={prompt} 68 | /> 69 | 70 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | ) 88 | } 89 | -------------------------------------------------------------------------------- /admin/src/components/PluginIcon/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * PluginIcon 4 | * 5 | */ 6 | 7 | import React from 'react'; 8 | import styled from 'styled-components'; 9 | import { Icon } from '@strapi/design-system/Icon'; 10 | import { Flex } from '@strapi/design-system/Flex'; 11 | import Pencil from '@strapi/icons/Pencil'; 12 | 13 | 14 | const IconBox = styled(Flex)` 15 | /* Hard code color values */ 16 | /* to stay consistent between themes */ 17 | background-color: #f0f0ff; /* primary100 */ 18 | border: 1px solid #d9d8ff; /* primary200 */ 19 | svg > path { 20 | fill: #4945ff; /* primary600 */ 21 | } 22 | `; 23 | 24 | const PluginIcon = () => { 25 | return ( 26 | 27 | 28 | 29 | ); 30 | }; 31 | 32 | export default PluginIcon; -------------------------------------------------------------------------------- /admin/src/index.js: -------------------------------------------------------------------------------- 1 | import { prefixPluginTranslations } from '@strapi/helper-plugin'; 2 | import pluginPkg from '../../package.json'; 3 | import pluginId from './pluginId'; 4 | import PluginIcon from './components/PluginIcon'; 5 | 6 | 7 | const name = pluginPkg.strapi.name; 8 | 9 | export default { 10 | register(app) { 11 | 12 | app.customFields.register({ 13 | name: "text-ai", 14 | pluginId: "ai-text-generation", 15 | type: "string", 16 | intlLabel: { 17 | id: "ai-text-generation.text-ai.label", 18 | defaultMessage: "Text AI", 19 | }, 20 | intlDescription: { 21 | id: "ai-text-generation.text-ai.description", 22 | defaultMessage: "Let AI do your writing!", 23 | }, 24 | icon: PluginIcon, // don't forget to create/import your icon component 25 | components: { 26 | Input: async () => import(/* webpackChunkName: "input-component" */ "./components/Input"), 27 | }, 28 | options: { 29 | base: [ 30 | /* 31 | Declare settings to be added to the "Base settings" section 32 | of the field in the Content-Type Builder 33 | */ 34 | { 35 | sectionTitle: { 36 | id: 'ai-text-generation.text-ai.length', 37 | defaultMessage: 'Text Length', 38 | }, 39 | items: [ // Add settings items to the section 40 | { 41 | 42 | intlLabel: { 43 | id: 'ai-text-generation.text-ai.length.label', 44 | defaultMessage: 'Select the length of your text', 45 | }, 46 | name: 'options.length', 47 | type: 'select', 48 | value: '64', // these are tokens, 1 token is roughly 4 english words so this goes to approx 250 words 49 | options: [ 50 | { 51 | key: '250 words', 52 | value: '250', 53 | metadatas: { 54 | intlLabel: { 55 | id: 'ai-text-generation.text-ai.length.250', 56 | defaultMessage: '250 words', 57 | }, 58 | }, 59 | }, 60 | { 61 | key: '500 words', 62 | value: '128', // these are tokens, 1 token is roughly 4 english words so this goes to approx 500 words 63 | metadatas: { 64 | intlLabel: { 65 | id: 'ai-text-generation.text-ai.length.500', 66 | defaultMessage: '500 words', 67 | }, 68 | }, 69 | }, 70 | ], 71 | }, 72 | ], 73 | }, 74 | ], 75 | }, 76 | }); 77 | }, 78 | 79 | 80 | 81 | bootstrap(app) {}, 82 | async registerTrads({ locales }) { 83 | const importedTrads = await Promise.all( 84 | locales.map((locale) => { 85 | return import( 86 | /* webpackChunkName: "translation-[request]" */ `./translations/${locale}.json` 87 | ) 88 | .then(({ default: data }) => { 89 | return { 90 | data: prefixPluginTranslations(data, pluginId), 91 | locale, 92 | }; 93 | }) 94 | .catch(() => { 95 | return { 96 | data: {}, 97 | locale, 98 | }; 99 | }); 100 | }) 101 | ); 102 | 103 | return Promise.resolve(importedTrads); 104 | }, 105 | }; 106 | -------------------------------------------------------------------------------- /admin/src/pages/App/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * This component is the skeleton around the actual pages, and should only 4 | * contain code that should be seen on all pages. (e.g. navigation bar) 5 | * 6 | */ 7 | 8 | import React from 'react'; 9 | import { Switch, Route } from 'react-router-dom'; 10 | import { NotFound } from '@strapi/helper-plugin'; 11 | import pluginId from '../../pluginId'; 12 | import HomePage from '../HomePage'; 13 | 14 | const App = () => { 15 | return ( 16 |
17 | 18 | 19 | 20 | 21 |
22 | ); 23 | }; 24 | 25 | export default App; 26 | -------------------------------------------------------------------------------- /admin/src/pages/HomePage/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * HomePage 4 | * 5 | */ 6 | 7 | import React from 'react'; 8 | // import PropTypes from 'prop-types'; 9 | import pluginId from '../../pluginId'; 10 | 11 | const HomePage = () => { 12 | return ( 13 |
14 |

{pluginId}'s HomePage

15 |

Happy coding

16 |
17 | ); 18 | }; 19 | 20 | export default HomePage; 21 | -------------------------------------------------------------------------------- /admin/src/pluginId.js: -------------------------------------------------------------------------------- 1 | import pluginPkg from '../../package.json'; 2 | 3 | const pluginId = pluginPkg.name.replace(/^(@[^-,.][\w,-]+\/|strapi-)plugin-/i, ''); 4 | 5 | export default pluginId; 6 | -------------------------------------------------------------------------------- /admin/src/translations/en.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /admin/src/translations/fr.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /admin/src/utils/axiosInstance.js: -------------------------------------------------------------------------------- 1 | /** 2 | * axios with a custom config. 3 | */ 4 | 5 | import axios from 'axios'; 6 | import { auth } from '@strapi/helper-plugin'; 7 | 8 | const instance = axios.create({ 9 | baseURL: process.env.STRAPI_ADMIN_BACKEND_URL, 10 | }); 11 | 12 | instance.interceptors.request.use( 13 | async (config) => { 14 | config.headers = { 15 | Authorization: `Bearer ${auth.getToken()}`, 16 | Accept: 'application/json', 17 | 'Content-Type': 'application/json', 18 | }; 19 | 20 | return config; 21 | }, 22 | (error) => { 23 | Promise.reject(error); 24 | } 25 | ); 26 | 27 | instance.interceptors.response.use( 28 | (response) => response, 29 | (error) => { 30 | // whatever you want to do with the error 31 | if (error.response?.status === 401) { 32 | auth.clearAppStorage(); 33 | window.location.reload(); 34 | } 35 | 36 | throw error; 37 | } 38 | ); 39 | 40 | export default instance; 41 | -------------------------------------------------------------------------------- /admin/src/utils/getTrad.js: -------------------------------------------------------------------------------- 1 | import pluginId from '../pluginId'; 2 | 3 | const getTrad = (id) => `${pluginId}.${id}`; 4 | 5 | export default getTrad; 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ai-text-generation", 3 | "version": "0.1.1", 4 | "description": "A Strapi Custom Field for Text Generation with Open AI.", 5 | "strapi": { 6 | "name": "ai-text-generation", 7 | "description": "A Strapi Custom Field for Text Generation with Open AI.", 8 | "kind": "plugin", 9 | "displayName": "AI Text Generation" 10 | }, 11 | "dependencies": { 12 | "axios": "^1.2.2" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "https://github.com/malgamves/strapi-open-ai-text-generation.git" 17 | }, 18 | "author": { 19 | "name": "Daniel Phiri", 20 | "email": "malgamves@gmail.com", 21 | "url": "https://malgamves.dev" 22 | }, 23 | "maintainers": [ 24 | { 25 | "name": "Daniel Phiri", 26 | "email": "malgamves@gmail.com", 27 | "url": "https://malgamves.dev" 28 | } 29 | ], 30 | "engines": { 31 | "node": ">=14.19.1 <=18.x.x", 32 | "npm": ">=6.0.0" 33 | }, 34 | "license": "MIT" 35 | } 36 | -------------------------------------------------------------------------------- /server/bootstrap.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = ({ strapi }) => { 4 | // bootstrap phase 5 | }; 6 | -------------------------------------------------------------------------------- /server/config/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | default: {}, 5 | validator() {}, 6 | }; 7 | -------------------------------------------------------------------------------- /server/content-types/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = {}; 4 | -------------------------------------------------------------------------------- /server/controllers/ai-controller.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = ({ strapi }) => ({ 4 | async generate(ctx) { 5 | ctx.body = await strapi 6 | .plugin('ai-text-generation') 7 | .service('openAi') 8 | .generateText(ctx.request.body.prompt, ctx.request.body.max_tokens); 9 | }, 10 | }); 11 | -------------------------------------------------------------------------------- /server/controllers/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const aiController = require('./ai-controller'); 4 | 5 | module.exports = { 6 | aiController, 7 | }; 8 | -------------------------------------------------------------------------------- /server/destroy.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = ({ strapi }) => { 4 | // destroy phase 5 | }; 6 | -------------------------------------------------------------------------------- /server/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const register = require('./register'); 4 | const bootstrap = require('./bootstrap'); 5 | const destroy = require('./destroy'); 6 | const config = require('./config'); 7 | const contentTypes = require('./content-types'); 8 | const controllers = require('./controllers'); 9 | const routes = require('./routes'); 10 | const middlewares = require('./middlewares'); 11 | const policies = require('./policies'); 12 | const services = require('./services'); 13 | 14 | module.exports = { 15 | register, 16 | bootstrap, 17 | destroy, 18 | config, 19 | controllers, 20 | routes, 21 | services, 22 | contentTypes, 23 | policies, 24 | middlewares, 25 | }; 26 | -------------------------------------------------------------------------------- /server/middlewares/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = {}; 4 | -------------------------------------------------------------------------------- /server/policies/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = {}; 4 | -------------------------------------------------------------------------------- /server/register.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = ({ strapi }) => { 4 | strapi.customFields.register({ 5 | name: 'text-ai', 6 | plugin: 'ai-text-generation', 7 | type: 'string', 8 | }); 9 | }; 10 | -------------------------------------------------------------------------------- /server/routes/index.js: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | { 3 | method: 'POST', 4 | path: '/generate-text', 5 | handler: 'aiController.generate', 6 | config: { 7 | policies: [], 8 | }, 9 | }, 10 | ]; 11 | -------------------------------------------------------------------------------- /server/services/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const openAi = require('./open-ai'); 4 | 5 | module.exports = { 6 | openAi, 7 | }; 8 | -------------------------------------------------------------------------------- /server/services/open-ai.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const axios = require('axios'); 4 | 5 | module.exports = ({ strapi }) => ({ 6 | 7 | async generateText(prompt, max_tokens) { 8 | try { 9 | const response = await axios( 10 | { 11 | url: 'https://api.openai.com/v1/completions', 12 | method: 'POST', 13 | headers: { 14 | 'Content-Type': 'application/json', 15 | 'Authorization': `Bearer ${strapi.plugin('ai-text-generation').config('apiToken')}` 16 | }, 17 | data: JSON.stringify({ 18 | 'model': 'text-davinci-001', 19 | 'prompt': `${prompt}`, 20 | 'temperature': 0.4, 21 | 'max_tokens': parseFloat(max_tokens), 22 | 'top_p': 1, 23 | 'frequency_penalty': 0, 24 | 'presence_penalty': 0 25 | }) 26 | }) 27 | 28 | 29 | const result = await response.data; 30 | return result; 31 | } 32 | catch (err) { 33 | console.log(err.response) 34 | } 35 | 36 | } 37 | 38 | }); 39 | -------------------------------------------------------------------------------- /strapi-admin.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = require('./admin/src').default; 4 | -------------------------------------------------------------------------------- /strapi-server.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = require('./server'); 4 | -------------------------------------------------------------------------------- /yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | asynckit@^0.4.0: 6 | version "0.4.0" 7 | resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" 8 | integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== 9 | 10 | axios@^1.2.2: 11 | version "1.2.2" 12 | resolved "https://registry.yarnpkg.com/axios/-/axios-1.2.2.tgz#72681724c6e6a43a9fea860fc558127dbe32f9f1" 13 | integrity sha512-bz/J4gS2S3I7mpN/YZfGFTqhXTYzRho8Ay38w2otuuDR322KzFIWm/4W2K6gIwvWaws5n+mnb7D1lN9uD+QH6Q== 14 | dependencies: 15 | follow-redirects "^1.15.0" 16 | form-data "^4.0.0" 17 | proxy-from-env "^1.1.0" 18 | 19 | combined-stream@^1.0.8: 20 | version "1.0.8" 21 | resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" 22 | integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== 23 | dependencies: 24 | delayed-stream "~1.0.0" 25 | 26 | delayed-stream@~1.0.0: 27 | version "1.0.0" 28 | resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" 29 | integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== 30 | 31 | follow-redirects@^1.15.0: 32 | version "1.15.2" 33 | resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13" 34 | integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA== 35 | 36 | form-data@^4.0.0: 37 | version "4.0.0" 38 | resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452" 39 | integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww== 40 | dependencies: 41 | asynckit "^0.4.0" 42 | combined-stream "^1.0.8" 43 | mime-types "^2.1.12" 44 | 45 | mime-db@1.52.0: 46 | version "1.52.0" 47 | resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" 48 | integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== 49 | 50 | mime-types@^2.1.12: 51 | version "2.1.35" 52 | resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" 53 | integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== 54 | dependencies: 55 | mime-db "1.52.0" 56 | 57 | proxy-from-env@^1.1.0: 58 | version "1.1.0" 59 | resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" 60 | integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== 61 | --------------------------------------------------------------------------------