├── .env.example ├── .github └── workflows │ └── release.yml ├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── lib └── index.js ├── package.json ├── public ├── banner.png ├── filebase-provider.png ├── fleek-provider.png ├── lighthouse-provider.png ├── pinata-provider.png ├── screenshot.png └── web3-provider.png └── test └── index.test.js /.env.example: -------------------------------------------------------------------------------- 1 | FILEBASE_KEY="" 2 | FILEBASE_SECRET="" 3 | FILEBASE_BUCKET="" 4 | 5 | PINATA_JWT="" 6 | 7 | FLEEK_KEY="" 8 | FLEEK_SECRET="" 9 | FLEEK_BUCKET="" 10 | 11 | WEB3_TOKEN="" 12 | 13 | LIGHTHOUSE_TOKEN="" 14 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | 7 | jobs: 8 | publish-npm: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v3 12 | - uses: actions/setup-node@v3 13 | with: 14 | node-version: 16 15 | registry-url: https://registry.npmjs.org/ 16 | - run: npm install -g yarn 17 | - run: yarn install --frozen-lockfile 18 | - run: yarn publish 19 | env: 20 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 21 | -------------------------------------------------------------------------------- /.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 | *.lock 63 | 64 | 65 | ############################ 66 | # Logs and databases 67 | ############################ 68 | 69 | .tmp 70 | *.log 71 | *.sql 72 | *.sqlite 73 | 74 | 75 | ############################ 76 | # Misc. 77 | ############################ 78 | 79 | *# 80 | .idea 81 | nbproject 82 | 83 | 84 | ############################ 85 | # Node.js 86 | ############################ 87 | 88 | lib-cov 89 | lcov.info 90 | pids 91 | logs 92 | results 93 | build 94 | node_modules 95 | .node_history 96 | package-lock.json 97 | .env 98 | yarn.lock 99 | .txt -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 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 | *.lock 63 | 64 | 65 | ############################ 66 | # Logs and databases 67 | ############################ 68 | 69 | .tmp 70 | *.log 71 | *.sql 72 | *.sqlite 73 | 74 | 75 | ############################ 76 | # Misc. 77 | ############################ 78 | 79 | *# 80 | .idea 81 | nbproject 82 | 83 | 84 | ############################ 85 | # Node.js 86 | ############################ 87 | 88 | lib-cov 89 | lcov.info 90 | pids 91 | logs 92 | results 93 | build 94 | node_modules 95 | .node_history 96 | package-lock.json 97 | yarn.lock 98 | 99 | .env 100 | .txt 101 | public -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2022-present Alex Baker 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 14 | SOFTWARE. 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Strapi Provider Upload IPFS Storage 2 | 3 | strapi-provider-upload-ipfs-storage 4 | 5 | IPFS (Filebase, Pinata, Fleek, Web3, Lighthouse) provider for Strapi uploads. 6 | 7 | ## Installation 8 | 9 | ```bash 10 | # using yarn 11 | yarn add strapi-provider-upload-ipfs-storage 12 | 13 | # using npm 14 | npm install strapi-provider-upload-ipfs-storage --save 15 | ``` 16 | 17 | ### Providers Configuration 18 | 19 | `./config/plugins.js` 20 | 21 | ```js 22 | module.exports = ({ env }) => ({ 23 | // ... 24 | upload: { 25 | config: { 26 | provider: "strapi-provider-upload-ipfs-storage", 27 | providerOptions: { 28 | defaultStorage: "filebase", 29 | filebase: { 30 | // https://console.filebase.com/keys 31 | key: env("FILEBASE_KEY"), 32 | secret: env("FILEBASE_SECRET"), 33 | bucket: env("FILEBASE_BUCKET"), 34 | }, 35 | pinata: { 36 | // https://app.pinata.cloud/keys 37 | jwt: env("PINATA_JWT"), 38 | }, 39 | fleek: { 40 | // https://app.fleek.co/#/settings/general/profile 41 | key: env("FLEEK_KEY"), 42 | secret: env("FLEEK_SECRET"), 43 | bucket: env("FLEEK_BUCKET"), 44 | }, 45 | web3: { 46 | // https://web3.storage/tokens/ 47 | token: env("WEB3_TOKEN"), 48 | }, 49 | lighthouse: { 50 | // https://files.lighthouse.storage/dashboard/apikey 51 | token: env("LIGHTHOUSE_TOKEN"), 52 | }, 53 | }, 54 | }, 55 | }, 56 | // ... 57 | }); 58 | ``` 59 | 60 | `.env` 61 | 62 | ```bash 63 | FILEBASE_KEY="" 64 | FILEBASE_SECRET="" 65 | FILEBASE_BUCKET="" 66 | 67 | PINATA_JWT="" 68 | 69 | FLEEK_KEY="" 70 | FLEEK_SECRET="" 71 | FLEEK_BUCKET="" 72 | 73 | WEB3_TOKEN="" 74 | 75 | LIGHTHOUSE_TOKEN="" 76 | ``` 77 | 78 | ## Configuration Strapi + Filebase [ [tutorial](https://docs.filebase.com/configurations/third-party-configurations/backup-client-configurations/strapi-provider-plugin) ] 79 | 80 | Configuration Strapi + Filebase 81 | 82 | | Variable | Type | Description | Required | 83 | | -------- | ------ | ---------------------- | -------- | 84 | | key | string | Filebase access key | yes | 85 | | secret | string | Filebase access secret | yes | 86 | | bucket | string | Filebase bucket name | yes | 87 | 88 | ```js 89 | module.exports = ({ env }) => ({ 90 | // ... 91 | upload: { 92 | config: { 93 | provider: "strapi-provider-upload-ipfs-storage", 94 | providerOptions: { 95 | defaultStorage: "filebase", 96 | filebase: { 97 | // https://console.filebase.com/keys 98 | key: env("FILEBASE_KEY"), 99 | secret: env("FILEBASE_SECRET"), 100 | bucket: env("FILEBASE_BUCKET"), 101 | }, 102 | }, 103 | }, 104 | }, 105 | // ... 106 | }); 107 | ``` 108 | 109 | ## Configuration Strapi + Pinata 110 | 111 | Configuration Strapi + Pinata 112 | 113 | | Variable | Type | Description | Required | 114 | | -------- | ------ | -------------------------------- | -------- | 115 | | jwt | string | Pinata JWT (Secret access token) | yes | 116 | 117 | ```js 118 | module.exports = ({ env }) => ({ 119 | // ... 120 | upload: { 121 | config: { 122 | provider: "strapi-provider-upload-ipfs-storage", 123 | providerOptions: { 124 | defaultStorage: "pinata", 125 | pinata: { 126 | // https://app.pinata.cloud/keys 127 | jwt: env("PINATA_JWT"), 128 | }, 129 | }, 130 | }, 131 | }, 132 | // ... 133 | }); 134 | ``` 135 | 136 | ## Configuration Strapi + Fleek 137 | 138 | Configuration Strapi + Fleek 139 | 140 | | Variable | Type | Description | Required | 141 | | -------- | ------ | -------------------------------------- | -------- | 142 | | key | string | Fleek Storage API key | yes | 143 | | secret | string | Fleek Storage API secret | yes | 144 | | bucket | string | Fleek bucket name (e.g. 71a...-bucket) | yes | 145 | 146 | ```js 147 | module.exports = ({ env }) => ({ 148 | // ... 149 | upload: { 150 | config: { 151 | provider: "strapi-provider-upload-ipfs-storage", 152 | providerOptions: { 153 | defaultStorage: "fleek", 154 | fleek: { 155 | // https://app.fleek.co/#/settings/general/profile 156 | key: env("FLEEK_KEY"), 157 | secret: env("FLEEK_SECRET"), 158 | bucket: env("FLEEK_BUCKET"), 159 | }, 160 | }, 161 | }, 162 | }, 163 | // ... 164 | }); 165 | ``` 166 | 167 | ## Configuration Strapi + Web3 168 | 169 | Configuration Strapi + Web3 170 | 171 | | Variable | Type | Description | Required | 172 | | -------- | ------ | ---------------------- | -------- | 173 | | token | string | Web3 Storage API Token | yes | 174 | 175 | ```js 176 | module.exports = ({ env }) => ({ 177 | // ... 178 | upload: { 179 | config: { 180 | provider: "strapi-provider-upload-ipfs-storage", 181 | providerOptions: { 182 | defaultStorage: "web3", 183 | web3: { 184 | // https://web3.storage/tokens/ 185 | token: env("WEB3_TOKEN"), 186 | }, 187 | }, 188 | }, 189 | }, 190 | // ... 191 | }); 192 | ``` 193 | 194 | ## Configuration Strapi + Lighthouse 195 | 196 | Configuration Strapi + Lighthouse 197 | 198 | | Variable | Type | Description | Required | 199 | | -------- | ------ | ---------------------------- | -------- | 200 | | token | string | Lighthouse Storage API Token | yes | 201 | 202 | ```js 203 | module.exports = ({ env }) => ({ 204 | // ... 205 | upload: { 206 | config: { 207 | provider: "strapi-provider-upload-ipfs-storage", 208 | providerOptions: { 209 | defaultStorage: "lighthouse", 210 | lighthouse: { 211 | // https://files.lighthouse.storage/dashboard/apikey 212 | token: env("LIGHTHOUSE_TOKEN"), 213 | }, 214 | }, 215 | }, 216 | }, 217 | // ... 218 | }); 219 | ``` 220 | 221 | ### Security Middleware Configuration 222 | 223 | Due to the default settings in the Strapi Security Middleware you will need to modify the `contentSecurityPolicy` settings to properly see thumbnail previews in the Media Library. You should replace `strapi::security` string with the object bellow instead as explained in the [middleware configuration](https://docs.strapi.io/developer-docs/latest/setup-deployment-guides/configurations/required/middlewares.html#loading-order) documentation. 224 | 225 | `./config/middlewares.js` 226 | 227 | ```js 228 | module.exports = [ 229 | // ... 230 | { 231 | name: "strapi::security", 232 | config: { 233 | contentSecurityPolicy: { 234 | useDefaults: true, 235 | directives: { 236 | "connect-src": ["'self'", "https:"], 237 | "img-src": [ 238 | "'self'", 239 | "data:", 240 | "blob:", 241 | "dl.airtable.com", 242 | "*.ipfs.dweb.link", // ipfs.tech 243 | "*.ipfs.cf-ipfs.com", // cloudflare.com 244 | "*.ipfs.w3s.link", // web3.storage 245 | ], 246 | "media-src": [ 247 | "'self'", 248 | "data:", 249 | "blob:", 250 | "dl.airtable.com", 251 | "*.ipfs.dweb.link", // ipfs.tech 252 | "*.ipfs.cf-ipfs.com", // cloudflare.com 253 | "*.ipfs.w3s.link", // web3.storage 254 | ], 255 | upgradeInsecureRequests: null, 256 | }, 257 | }, 258 | }, 259 | }, 260 | // ... 261 | ]; 262 | ``` 263 | 264 | ## Links 265 | 266 | - [Strapi website](https://strapi.io/) 267 | - [IPFS website](https://ipfs.tech/) 268 | - [Filebase website](https://filebase.com/) 269 | - [Pinata website](https://pinata.cloud/) 270 | - [Fleek website](https://fleek.co/) 271 | - [Web3 website](https://web3.storage/) 272 | - [Lighthouse website](https://lighthouse.storage/) 273 | 274 | --- 275 | 276 | `(c)` Alex Baker 277 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /** 4 | * Module dependencies 5 | */ 6 | 7 | /* eslint-disable no-unused-vars */ 8 | // Public node modules. 9 | const ipfs = require("ipfs-storage"); 10 | 11 | module.exports = { 12 | init(config) { 13 | return { 14 | uploadStream(file) { 15 | return new Promise(async (resolve, reject) => { 16 | try { 17 | const url = await ipfs.uploadFile[config.defaultStorage]( 18 | config[config.defaultStorage], 19 | file 20 | ); 21 | file.url = url; 22 | resolve(); 23 | } catch (e) { 24 | reject(e); 25 | } 26 | }); 27 | }, 28 | upload(file) { 29 | return new Promise(async (resolve, reject) => { 30 | try { 31 | const url = await ipfs.uploadFile[config.defaultStorage]( 32 | config[config.defaultStorage], 33 | file 34 | ); 35 | file.url = url; 36 | resolve(); 37 | } catch (e) { 38 | reject(e); 39 | } 40 | }); 41 | }, 42 | delete(file) { 43 | return new Promise(async (resolve, reject) => { 44 | try { 45 | await ipfs.deleteFile[config.defaultStorage]( 46 | config[config.defaultStorage], 47 | file 48 | ); 49 | resolve(); 50 | } catch (e) { 51 | reject(e); 52 | } 53 | }); 54 | }, 55 | }; 56 | }, 57 | }; 58 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "strapi-provider-upload-ipfs-storage", 3 | "version": "1.1.0", 4 | "description": "IPFS upload provider for Strapi", 5 | "keywords": [ 6 | "upload", 7 | "ipfs", 8 | "web3", 9 | "filebase", 10 | "pinata", 11 | "fleek", 12 | "strapi", 13 | "lighthouse" 14 | ], 15 | "homepage": "https://github.com/alexbakers/strapi-provider-upload-ipfs-storage", 16 | "bugs": { 17 | "url": "https://github.com/alexbakers/strapi-provider-upload-ipfs-storage/issues" 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "git://github.com/alexbakers/strapi-provider-upload-ipfs-storage.git" 22 | }, 23 | "license": "MIT", 24 | "main": "./lib/index.js", 25 | "directories": { 26 | "lib": "./lib" 27 | }, 28 | "scripts": { 29 | "test": "node ./test/index.test.js" 30 | }, 31 | "dependencies": { 32 | "ipfs-storage": "^1.2.1" 33 | }, 34 | "peerDependencies": { 35 | "@strapi/strapi": "^4.5.1" 36 | }, 37 | "engines": { 38 | "node": ">=14.19.1 <=18.x.x", 39 | "npm": ">=6.0.0" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /public/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexbakers/strapi-provider-upload-ipfs-storage/62f8570ad20693cbd24661868070b88fe6e3e8f4/public/banner.png -------------------------------------------------------------------------------- /public/filebase-provider.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexbakers/strapi-provider-upload-ipfs-storage/62f8570ad20693cbd24661868070b88fe6e3e8f4/public/filebase-provider.png -------------------------------------------------------------------------------- /public/fleek-provider.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexbakers/strapi-provider-upload-ipfs-storage/62f8570ad20693cbd24661868070b88fe6e3e8f4/public/fleek-provider.png -------------------------------------------------------------------------------- /public/lighthouse-provider.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexbakers/strapi-provider-upload-ipfs-storage/62f8570ad20693cbd24661868070b88fe6e3e8f4/public/lighthouse-provider.png -------------------------------------------------------------------------------- /public/pinata-provider.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexbakers/strapi-provider-upload-ipfs-storage/62f8570ad20693cbd24661868070b88fe6e3e8f4/public/pinata-provider.png -------------------------------------------------------------------------------- /public/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexbakers/strapi-provider-upload-ipfs-storage/62f8570ad20693cbd24661868070b88fe6e3e8f4/public/screenshot.png -------------------------------------------------------------------------------- /public/web3-provider.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexbakers/strapi-provider-upload-ipfs-storage/62f8570ad20693cbd24661868070b88fe6e3e8f4/public/web3-provider.png -------------------------------------------------------------------------------- /test/index.test.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const { join } = require("path"); 3 | const ipfs = require("ipfs-storage"); 4 | 5 | require("dotenv").config(); 6 | 7 | fs.readFile(join(__dirname, "..", "banner.png"), async (err, data) => { 8 | if (err) { 9 | console.log("🆘 ERROR:", err); 10 | return; 11 | } 12 | 13 | if ( 14 | !process.env.FILEBASE_KEY && 15 | !process.env.PINATA_JWT && 16 | !process.env.FLEEK_KEY && 17 | !process.env.WEB3_TOKEN && 18 | !process.env.LIGHTHOUSE_TOKEN 19 | ) { 20 | console.log("🆘 ERROR:", "Create .env file"); 21 | return; 22 | } 23 | 24 | try { 25 | const url = await ipfs.uploadFile.filebase( 26 | { 27 | key: process.env.FILEBASE_KEY, 28 | secret: process.env.FILEBASE_SECRET, 29 | bucket: process.env.FILEBASE_BUCKET, 30 | }, 31 | { 32 | hash: "banner", 33 | ext: ".png", 34 | buffer: data, 35 | } 36 | ); 37 | console.log("✅ FILEBASE:", url); 38 | await ipfs.deleteFile.filebase( 39 | { 40 | key: process.env.FILEBASE_KEY, 41 | secret: process.env.FILEBASE_SECRET, 42 | bucket: process.env.FILEBASE_BUCKET, 43 | }, 44 | { 45 | hash: "banner", 46 | ext: ".png", 47 | } 48 | ); 49 | } catch (err) { 50 | console.log("🆘 FILEBASE:", err); 51 | } 52 | 53 | try { 54 | const url = await ipfs.uploadFile.pinata( 55 | { 56 | jwt: process.env.PINATA_JWT, 57 | }, 58 | { 59 | hash: "banner", 60 | ext: ".png", 61 | buffer: data, 62 | } 63 | ); 64 | console.log("✅ PINATA:", url); 65 | await ipfs.deleteFile.pinata( 66 | { 67 | jwt: process.env.PINATA_JWT, 68 | }, 69 | { 70 | url, 71 | } 72 | ); 73 | } catch (err) { 74 | console.log("🆘 PINATA:", err); 75 | } 76 | 77 | try { 78 | const url = await ipfs.uploadFile.fleek( 79 | { 80 | key: process.env.FLEEK_KEY, 81 | secret: process.env.FLEEK_SECRET, 82 | bucket: process.env.FLEEK_BUCKET, 83 | }, 84 | { 85 | hash: "banner", 86 | ext: ".png", 87 | buffer: data, 88 | } 89 | ); 90 | console.log("✅ FLEEK:", url); 91 | await ipfs.deleteFile.fleek( 92 | { 93 | key: process.env.FLEEK_KEY, 94 | secret: process.env.FLEEK_SECRET, 95 | bucket: process.env.FLEEK_BUCKET, 96 | }, 97 | { 98 | hash: "banner", 99 | ext: ".png", 100 | } 101 | ); 102 | } catch (err) { 103 | console.log("🆘 FLEEK:", err); 104 | } 105 | 106 | try { 107 | const url = await ipfs.uploadFile.web3( 108 | { 109 | token: process.env.WEB3_TOKEN, 110 | }, 111 | { 112 | hash: "banner", 113 | ext: ".png", 114 | buffer: data, 115 | } 116 | ); 117 | console.log("✅ WEB3:", url); 118 | await ipfs.deleteFile.web3( 119 | { 120 | token: process.env.WEB3_TOKEN, 121 | }, 122 | { 123 | url, 124 | } 125 | ); 126 | } catch (err) { 127 | console.log("🆘 WEB3:", err); 128 | } 129 | 130 | try { 131 | const url = await ipfs.uploadFile.lighthouse( 132 | { 133 | token: process.env.LIGHTHOUSE_TOKEN, 134 | }, 135 | { 136 | hash: "banner", 137 | ext: ".png", 138 | buffer: data, 139 | } 140 | ); 141 | console.log("✅ LIGHTHOUSE:", url); 142 | await ipfs.deleteFile.lighthouse( 143 | { 144 | token: process.env.LIGHTHOUSE_TOKEN, 145 | }, 146 | { 147 | url, 148 | } 149 | ); 150 | } catch (err) { 151 | console.log("🆘 LIGHTHOUSE:", err); 152 | } 153 | }); 154 | --------------------------------------------------------------------------------