Repository: cloudflare/serverless-cloudflare-workers Branch: master Commit: e93ec702290b Files: 27 Total size: 68.1 KB Directory structure: gitextract_3btj622v/ ├── .github/ │ └── workflows/ │ └── semgrep.yml ├── .gitignore ├── LICENSE.md ├── README.md ├── deploy/ │ ├── cloudflareDeploy.js │ ├── cloudflareDeployFunction.js │ └── lib/ │ ├── logResponse.js │ ├── multiscript.js │ ├── singlescript.js │ └── workerScript.js ├── index.js ├── invoke/ │ ├── cloudflareInvoke.js │ └── lib/ │ └── invoke.js ├── package.json ├── provider/ │ ├── cloudflareProvider.js │ ├── credentials.js │ └── sdk.js ├── remove/ │ ├── cloudflareRemove.js │ └── lib/ │ └── singlescript.js ├── shared/ │ ├── accountType.js │ ├── duplicate.js │ ├── multiscript.js │ └── validate.js ├── test/ │ └── shared/ │ ├── duplicate_test.js │ └── multiscript_test.js └── utils/ ├── index.js └── webpack.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/workflows/semgrep.yml ================================================ on: pull_request: {} workflow_dispatch: {} push: branches: - main - master schedule: - cron: '0 0 * * *' name: Semgrep config jobs: semgrep: name: semgrep/ci runs-on: ubuntu-20.04 env: SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_APP_TOKEN }} SEMGREP_URL: https://cloudflare.semgrep.dev SEMGREP_APP_URL: https://cloudflare.semgrep.dev SEMGREP_VERSION_CHECK_URL: https://cloudflare.semgrep.dev/api/check-version container: image: returntocorp/semgrep steps: - uses: actions/checkout@v3 - run: semgrep ci ================================================ FILE: .gitignore ================================================ .nyc_output coverage/ node_modules/ .npmrc *.DS_Store ================================================ FILE: LICENSE.md ================================================ Copyright 2018 Cloudflare Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ================================================ FILE: README.md ================================================ # serverless-cloudflare-workers Serverless plugin for [Cloudflare Workers](https://developers.cloudflare.com/workers/) ## Documentation https://serverless.com/framework/docs/providers/cloudflare/guide/quick-start/ ### Bundling with Webpack You can have the plugin automatically bundle your code into one file using [webpack](https://webpack.js.org/). This is a great solution if you are fine with a no frills bundling. You can use a single global webpack config to bundle your assets. And this webpack config will be built during the packaging time, before individual functions are prepared. To use this, add `webpackConfig` to your service section in serverless config, with value as the path to the webpack config. ```yaml service: name: service-name webpackConfig: webpack.config #webpack config path without js extension from root folder. config: accountId: ${env:CLOUDFLARE_ACCOUNT_ID} zoneId: ${env:CLOUDFLARE_ZONE_ID} ``` You can also add a function level webpack configuration in addition to a global webpack configuration. This helps you to process bundling different for an individual function than the global webpack config explained earlier. To use this, set the webpack config path to the function level `webpack` variable. Setting function level `webpack` variable to `true` will force webpack to bundle the function script with a default web pack configuration. Setting `webpack` key to `false` will turn off webpack for the function. (i.e the function script will not be fetched from dist folder) Simply add `webpack: true | ` to your config block. ```yaml functions: myfunction: name: myfunction webpack: true #or the web pack config path for this function script: handlers/myfunctionhandler events: - http: url: example.com/myfunction method: GET ``` ### Environment Variables While Cloudflare Workers doesn't exactly offer environment vairables, we can bind global variables to values, essentially giving the same capabilities. In your function configuration, add key value pairs in `environment` ```yaml functions: myFunction: environment: MYKEY: value_of_my_key ANOTHER_KEY_OF_MINE: sweet_child_o_mine ``` Then in your script, you can reference `MYKEY` to access the value. You can also add an environment block under `provider`. These will get added to every function. If a function defines the same variable, the function defintion will overwrite the provider block definition. ```yaml provider: name: cloudflare environment: MYKEY: value_of_my_key ANOTHER_KEY_OF_MINE: sweet_child_o_mine ``` ### Using Cloudflare KV Storage The plugin can create and bind a [KV Storage](https://developers.cloudflare.com/workers/kv/) namespace for your function by simpling adding a resources section. The following will create a namespace called `BEST_NAMESPACE` and bind the variable `TEST` to that namespace inside `myfunction`. ```yaml functions: myfunction: name: myfunction webpack: true script: handlers/myfunctionhandler resources: kv: - variable: TEST namespace: BEST_NAMESPACE events: - http: url: example.com/myfunction method: GET ``` ### Web Assembly The plugin can upload and bind WASM to execute in your worker. The easiest way to do this is to use the --template cloudflare-workers-rust when generating a project. The template includes a Rust create folder setup with wasm-pack, a webpack script for adding the generated javascript into your project, and the yml file settings to upload the wasm file itself. ```yaml functions: myfunction: name: myfunction webpack: true script: handlers/myfunctionhandler resources: wasm: - variable: WASM filename: rust/pkg/wasm_bg.wasm events: - http: url: example.com/myfunction method: GET ``` ================================================ FILE: deploy/cloudflareDeploy.js ================================================ /** * Copyright (c) 2018, Cloudflare. All rights reserved. * * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER * IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ const BB = require("bluebird"); const ms = require("./lib/multiscript"); const ss = require("./lib/singlescript"); const accountType = require("../shared/accountType"); const logs = require("./lib/logResponse"); const utils = require("../utils"); const validate = require("../shared/validate"); const duplicate = require("../shared/duplicate"); class CloudflareDeploy { constructor(serverless, options) { this.serverless = serverless; this.options = options || {}; this.provider = this.serverless.getProvider("cloudflare"); Object.assign(this, accountType, ms, utils, ss, logs, validate); const startTime = Date.now(); this.hooks = { "deploy:deploy": () => BB.bind(this) .then(this.checkAccountType) .then(async isMultiScript => { this.serverless.cli.log('Starting Serverless Cloudflare-Worker deployment.'); if (isMultiScript && await duplicate.checkIfDuplicateRoutes(this.serverless, this.provider)) { return BB.reject("Duplicate routes pointing to different script"); } if (this.getInvalidScriptNames()) { return BB.reject( "Worker names can contain lowercase letters, numbers, underscores, and dashes. They cannot start with dashes." ); } if (isMultiScript) { return this.multiScriptDeployAll() } else { const functionObject = this.getFunctionObjectForSingleScript(); return this.deploySingleScript(functionObject); } }) .then(this.logDeployResponse) .then(k => this.serverless.cli.log(`Finished deployment in ${(Date.now() - startTime) / 1000} seconds.`)) .then(k => this.serverless.cli.log('Finished Serverless Cloudflare-Worker deployment.')) }; } } module.exports = CloudflareDeploy; ================================================ FILE: deploy/cloudflareDeployFunction.js ================================================ /** * Copyright (c) 2018, Cloudflare. All rights reserved. * * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER * IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ const BB = require("bluebird"); const ms = require("./lib/multiscript"); const ss = require("./lib/singlescript"); const validateFunctionName = require("../shared/validate"); const accountType = require("../shared/accountType"); const utils = require("../utils"); const logs = require("./lib/logResponse"); const duplicate = require("../shared/duplicate"); class CloudflareDeployFunction { constructor(serverless, options) { this.serverless = serverless; this.options = options || {}; this.provider = this.serverless.getProvider("cloudflare"); Object.assign(this, validateFunctionName, accountType, ms, utils, ss, logs); let functionNamesForDeploy = []; this.hooks = { "deploy:function:deploy": () => BB.bind(this) .then(this.validateFunctionName) .then( (functions) => { functionNamesForDeploy = functions; return this.checkAccountType; }) .then(async isMultiScript => { if (isMultiScript && await duplicate.checkIfDuplicateRoutes(this.serverless, this.provider)) { return BB.reject("Duplicate routes pointing to different script"); } if (this.getInvalidScriptNames()) { return BB.reject( "Worker names can contain lowercase letters, numbers, underscores, and dashes. They cannot start with dashes." ); } if (isMultiScript) { return this.multiScriptDeployAll(functionNamesForDeploy); } else { const functionObject = this.getFunctionObjectForSingleScript(); return this.deploySingleScript(functionObject); } }) .then(this.logDeployResponse) }; } } module.exports = CloudflareDeployFunction; ================================================ FILE: deploy/lib/logResponse.js ================================================ /** * Copyright (c) 2018, Cloudflare. All rights reserved. * * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER * IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ module.exports = { logDeployResponse({ workerScriptResponse, routesResponse, namespaceResponses, isMultiScript }) { if (isMultiScript) { this.aggregateWorkerResponse(this.serverless.cli, workerScriptResponse); this.aggregateRoutesResponse(this.serverless.cli, routesResponse); } else { this.parseWorkerResponse(this.serverless.cli, workerScriptResponse); this.parseRoutesResponse(this.serverless.cli, routesResponse); } } }; ================================================ FILE: deploy/lib/multiscript.js ================================================ /** * Copyright (c) 2018, Cloudflare. All rights reserved. * * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER * IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ const BB = require("bluebird"); const webpack = require("../../utils/webpack"); const ms = require("../../shared/multiscript"); module.exports = { async deployScriptToCloudflare(functionObject) { return BB.bind(this) .then(async () => { if (functionObject.webpack) { await webpack.pack(this.serverless, functionObject); } // deploy script, routes, and namespaces const namespaceResponse = await ms.deployNamespaces(this.provider.config.accountId, functionObject); const workerScriptResponse = await ms.deployWorker(this.provider.config.accountId, this.serverless, functionObject); const routesResponse = await ms.deployRoutes(this.provider.config.zoneId, functionObject); return { workerScriptResponse, routesResponse, namespaceResponse }; }); }, async deployScript(scriptName) { const startScriptTime = Date.now(); const functionObject = this.getFunctionObject(scriptName); this.serverless.cli.log(`deploying script: ${scriptName}`); const { workerScriptResponse, routesResponse: rResponse, namespaceResponse, } = await this.deployScriptToCloudflare(functionObject, scriptName); this.serverless.cli.log(`Finished deployment ${scriptName} in ${(Date.now() - startScriptTime) / 1000} seconds`); return { workerResponse: workerScriptResponse, routesResponse: rResponse, namespaceResponse } }, /** * Deploy functions passed in or all functions if no functions are submitted * * @param {Array[string]} functions */ async multiScriptDeployAll(functions = null) { functions = functions || this.serverless.service.getAllFunctions(); if (typeof (functions) === 'undefined' || functions === null) { throw new Error("Incorrect template being used for a MultiScript user "); } let workerResponse = []; let routesResponse = []; let namespaceResponses = []; // Build global webpack if available await webpack.packGlobalWebpack(this.serverless) this.serverless.cli.log('Starting deployment'); // scriptName is really the key of the function map for (const name of functions) { const result = await this.deployScript(name); workerResponse.push(result.workerResponse) routesResponse.push(result.routesResponse) namespaceResponses.push(result.namespaceResponse) } return { workerScriptResponse: workerResponse, routesResponse, namespaceResponses, isMultiScript: true }; } }; ================================================ FILE: deploy/lib/singlescript.js ================================================ /** * Copyright (c) 2018, Cloudflare. All rights reserved. * * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER * IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ const sdk = require("../../provider/sdk"); const { generateCode, generateWASM } = require("./workerScript"); const BB = require("bluebird"); const webpack = require("../../utils/webpack"); const cf = require("cloudflare-workers-toolkit"); const ms = require("../../shared/multiscript"); module.exports = { async singleServeRoutesAPI({ pattern, zoneId }) { const payload = { pattern, enabled: true }; return await sdk.cfApiCall({ url: `https://api.cloudflare.com/client/v4/zones/${zoneId}/workers/filters`, method: `POST`, contentType: `application/json`, body: JSON.stringify(payload) }); }, async getRoutesSingleScript(zoneId) { return sdk.cfApiCall({ url: `https://api.cloudflare.com/client/v4/zones/${zoneId}/workers/filters`, method: `GET` }); }, collectRoutes(events) { return events.map(event => { if (event.http) { return event.http.url; } }) }, /** * Deploys a single script zone which can only have one script per zone. * Because of this the name field is omitted. * Also, zoneId must be used since it is required to deploy a single script. * @param {*} functionObject */ async deploySingleScript(functionObject) { return await BB.bind(this).then(async () => { const { zoneId } = this.provider.config; const singleScriptRoutes = this.collectRoutes(functionObject.events); let workerScriptResponse; let routesResponse = []; // Build global webpack if available await webpack.packGlobalWebpack(this.serverless) // If a local webpack config defined, do that too if (functionObject.webpack) { await webpack.pack(this.serverless, functionObject); } const scriptContents = generateCode(this.serverless, functionObject); cf.setAccountId(this.provider.config.accountId); const namespaceResponse = await ms.deployNamespaces(this.provider.config.accountId, functionObject); let bindings = await ms.getBindings(this.provider, functionObject) const response = await cf.workers.deploy({ accountId: this.provider.config.accountId, zoneId, script: scriptContents, wasm: generateWASM(functionObject), bindings }); workerScriptResponse = response; for (const pattern of singleScriptRoutes) { this.serverless.cli.log(`deploying route: ${pattern}`); const rResponse = await this.singleServeRoutesAPI({ pattern, zoneId }); routesResponse.push(rResponse); } return { namespaceResponse, workerScriptResponse, routesResponse, isMultiScript: false }; }); } }; ================================================ FILE: deploy/lib/workerScript.js ================================================ /** * Copyright (c) 2018, Cloudflare. All rights reserved. * * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER * IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ const path = require("path"); const fs = require("fs"); const webpack = require("../../utils/webpack"); const generateCode = (serverless, functionObject) => { let { script } = functionObject; const rootPath = webpack.getAssetPathPrefix(serverless, functionObject) if (path.extname(script) != ".js") { script = `${rootPath}${script}.js` } return fs.readFileSync(script).toString(); }; /** * Builds the list of wasm objects for script deployment * @param {} functionObject */ const generateWASM = (functionObject) => { let wasm = []; if (functionObject && functionObject.resources && functionObject.resources.wasm) { functionObject.resources.wasm.map((w) => { wasm.push(w.file); }) } return wasm; } module.exports = { generateCode, generateWASM }; ================================================ FILE: index.js ================================================ /** * Copyright (c) 2018, Cloudflare. All rights reserved. * * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER * IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ const CloudflareDeploy = require("./deploy/cloudflareDeploy"); const CloudflareDeployFunction = require("./deploy/cloudflareDeployFunction"); const CloudflareRemove = require("./remove/cloudflareRemove"); const CloudflareProvider = require("./provider/cloudflareProvider"); const CloudflareInvoke = require("./invoke/cloudflareInvoke"); class CloudflareIndex { constructor(serverless, options) { this.serverless = serverless; this.options = options; this.serverless.pluginManager.addPlugin(CloudflareProvider); this.serverless.pluginManager.addPlugin(CloudflareDeploy); this.serverless.pluginManager.addPlugin(CloudflareDeployFunction); this.serverless.pluginManager.addPlugin(CloudflareRemove); this.serverless.pluginManager.addPlugin(CloudflareInvoke); } } module.exports = CloudflareIndex; ================================================ FILE: invoke/cloudflareInvoke.js ================================================ /** * Copyright (c) 2018, Cloudflare. All rights reserved. * * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER * IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ "use strict"; const BB = require("bluebird"); const sdk = require("../provider/sdk"); const validateFunctionName = require("../shared/validate"); const accountType = require("../shared/accountType"); const utils = require("../utils"); const invoke = require("./lib/invoke"); class CloudflareInvoke { constructor(serverless, options) { this.serverless = serverless; this.options = options || {}; this.provider = this.serverless.getProvider("cloudflare"); Object.assign(this, utils, validateFunctionName, accountType, invoke); this.hooks = { "invoke:invoke": () => BB.bind(this) .then(this.validateFunctionName) .then(this.bakeEvent) .then(async payload => { return await sdk.invokeApiCall(payload); }) .then(resp => { console.log(resp); }) }; } } module.exports = CloudflareInvoke; ================================================ FILE: invoke/lib/invoke.js ================================================ /** * Copyright (c) 2018, Cloudflare. All rights reserved. * * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER * IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ module.exports = { bakeEvent() { const functionObject = this.getFunctionObject(); const { events } = functionObject; let bakedEvent = { url: "", headers: {}, method: "GET" }; events.forEach(event => { const { http } = event; Object.keys(http).forEach(httpEvent => { if (httpEvent === "url") { let urlString = http[httpEvent]; if (!urlString.startsWith("http://") && !urlString.startsWith("https://")) { urlString = "https://" + urlString; } if (this.options.querystring) { urlString += "?" + this.options.querystring; } bakedEvent.url = urlString; } else if (httpEvent == "headers") { Object.keys(http[httpEvent]).forEach(key => { const value = http[httpEvent][key]; bakedEvent["headers"][key] = value; }); } else { bakedEvent[httpEvent] = http[httpEvent]; } }); }); return bakedEvent; } }; ================================================ FILE: package.json ================================================ { "name": "serverless-cloudflare-workers", "version": "1.2.0", "description": "serverless cloudflare workers ", "main": "index.js", "scripts": { "test": "nyc --reporter=html --reporter=text mocha --recursive", "fmt": "eslint --fix ./deploy ./invoke ./provider ./remove ./index.js" }, "repository": { "type": "git", "url": "git+https://github.com/cloudflare/serverless-cloudflare-workers.git" }, "keywords": [ "serverless", "serverless", "framework", "serverless", "applications", "workers", "cloudflare" ], "dependencies": { "bluebird": "^3.4.7", "cloudflare-workers-toolkit": "^0.1.0", "fs-extra": "^7.0.1", "node-fetch": "^2.3.0", "webpack": "^4.25.1" }, "author": "serverless.com", "license": "MIT", "bugs": { "url": "https://github.com/cloudflare/serverless-cloudflare-workers/issues" }, "homepage": "https://github.com/cloudflare/serverless-cloudflare-workers#readme", "devDependencies": { "eslint": "^5.13.0", "eslint-config-prettier": "^2.9.0", "eslint-plugin-prettier": "^2.6.2", "mocha": "^5.2.0", "nyc": "^13.3.0", "proxyquire": "^2.1.0" } } ================================================ FILE: provider/cloudflareProvider.js ================================================ /** * Copyright (c) 2018, Cloudflare. All rights reserved. * * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER * IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ "use strict"; const sdk = require("./sdk"); const providerName = "cloudflare"; class CloudflareProvider { static getProviderName() { return providerName; } constructor(serverless) { this.serverless = serverless; this.provider = this; this.serverless.setProvider(providerName, this); this.config = this.serverless.service.provider.config || serverless.service.serviceObject.config; } } module.exports = CloudflareProvider; ================================================ FILE: provider/credentials.js ================================================ /** * Copyright (c) 2018, Cloudflare. All rights reserved. * * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER * IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ "use strict"; const ENV_PARAMS = ["CLOUDFLARE_AUTH_KEY", "CLOUDFLARE_AUTH_EMAIL"]; const REQUIRED_CREDENTIALS = ENV_PARAMS.map(s => { // ["auth_key", "email"] const a = s.split("CLOUDFLARE_")[1]; return a.toLowerCase(); }); function get() { const envProps = {}; ENV_PARAMS.forEach(envName => { if (process.env[envName]) { envProps[envName.split("CLOUDFLARE_")[1].toLowerCase()] = process.env[envName]; } }); return envProps; } module.exports = { get, REQUIRED_CREDENTIALS }; ================================================ FILE: provider/sdk.js ================================================ /** * Copyright (c) 2018, Cloudflare. All rights reserved. * * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER * IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ const fetch = require('node-fetch'); const cfApiCall = async ({ url, method, contentType = null, body = null }) => { if (url.substring(0, 8) !== "https://") { url = `https://api.cloudflare.com/client/v4${url}`; } let options = { headers: { "X-Auth-Email": process.env.CLOUDFLARE_AUTH_EMAIL, "X-Auth-Key": process.env.CLOUDFLARE_AUTH_KEY }, method: method }; if (contentType) { options["headers"]["Content-Type"] = contentType; } if (body) { options["body"] = body; } return await fetch(url, options).then(responseBody => responseBody.json()); }; const invokeApiCall = async ({ url, method, contentType = null, body = null, headers }) => { let options = { method, headers }; if (contentType) { options["headers"]["Content-Type"] = contentType; } if (body) { options["body"] = body; } return await fetch(url, options).then(responseBody => responseBody.text()); }; module.exports = { cfApiCall, invokeApiCall }; ================================================ FILE: remove/cloudflareRemove.js ================================================ /** * Copyright (c) 2018, Cloudflare. All rights reserved. * * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER * IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ const BB = require("bluebird"); const ss = require("./lib/singlescript"); const accountType = require("../shared/accountType"); const cf = require("cloudflare-workers-toolkit"); const utils = require("../utils"); class CloudflareRemove { constructor(serverless, options) { this.serverless = serverless; this.options = options; this.provider = this.serverless.getProvider("cloudflare"); // TODO: refactor out Object assigns. they lead to difficult code Object.assign(this, accountType, utils); this.hooks = { "remove:remove": () => { if (this.options.function || this.options.f) { throw new Error("This does not support -f yet.") } BB.bind(this) .then(this.remove) .then(resp => console.log("Removed")) } }; } async remove() { const { accountId, zoneId, routes: singleScriptEnabled } = this.provider.config; const functionKeys = this.serverless.service.getAllFunctions(); const multiscriptEnabled = await this.checkAccountType(); if (multiscriptEnabled) { if (functionKeys === undefined || functionKeys === null) { throw new Error( "Incorrect template being used for a MultiScript user " ); } const { success, result, errors } = await cf.routes.getRoutes({zoneId}); let allRoutes = {}; if (success) { result.forEach(r => { try { allRoutes[r.script].push(r.id); } catch (err) { allRoutes[r.script] = [r.id]; } }); } else if (errors) { throw new Error(errors); } const promises = []; functionKeys.forEach(functionKey => { let serviceName = this.getFunctionObject(functionKey).name; allRoutes[serviceName].forEach(routeId => { promises.push(cf.routes.remove({zoneId, routeId})); }); promises.push(cf.workers.remove({accountId, name: serviceName})); }); await Promise.all(promises); } else { await Promise.all([cf.workers.remove({zoneId: this.provider.config.zoneId}), ss.removeRoutes(zoneId)]); } this.serverless.cli.log("removed routes + scripts"); return true; } } module.exports = CloudflareRemove; ================================================ FILE: remove/lib/singlescript.js ================================================ /** * Copyright (c) 2018, Cloudflare. All rights reserved. * * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER * IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ const sdk = require("../../provider/sdk"); module.exports = { async removeRoutes(zoneId) { const { success, errors, result } = await sdk.cfApiCall({ url: `https://api.cloudflare.com/client/v4/zones/${zoneId}/workers/filters`, contentType: `application/json` }); if (success) { let promises = []; result.forEach(filter => { promises.push( sdk.cfApiCall({ url: `https://api.cloudflare.com/client/v4/zones/${zoneId}/workers/filters/${ filter["id"] }`, method: `DELETE` }) ); }); return await Promise.all(promises); } throw new Error(errors); } }; ================================================ FILE: shared/accountType.js ================================================ /** * Copyright (c) 2018, Cloudflare. All rights reserved. * * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER * IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ const BB = require("bluebird"); const credentials = require("../provider/credentials"); const cf = require("cloudflare-workers-toolkit"); module.exports = { async checkAccountType() { const zoneId = this.provider.config.zoneId; return await BB.bind(this) .then(this.checkAllEnvironmentVariables) .then(function() { return cf.workers.getSettings({zoneId}); }) .then(this.checkIfMultiScript) }, checkAllEnvironmentVariables() { const envCreds = credentials.get(); const requiredCredentials = credentials.REQUIRED_CREDENTIALS; requiredCredentials.forEach(requiredCredential => { if (!envCreds[requiredCredential]) { return BB.reject( `Missing mandatory environment variable: CLOUDFLARE_${requiredCredential.toUpperCase()}.` ); } }); }, checkIfMultiScript({ success, errors, result }) { if (!success) { return BB.reject(JSON.stringify(errors)); } const { multiscript, enabled } = result; if (!enabled) { return BB.reject( "Workers are not enabled for this account, please upgrade your account at https://cloudflare.com" ); } return multiscript; } }; ================================================ FILE: shared/duplicate.js ================================================ /** * Copyright (c) 2018, Cloudflare. All rights reserved. * * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER * IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ const cf = require("cloudflare-workers-toolkit"); module.exports = { /** * @param {*} serverless * @param {*} provider */ async checkIfDuplicateRoutes(serverless, provider) { // for any worker that we are uploading, we check its routes in the yml file and // check if there are exact same routes in our cloudflare account which point to // different script name if (typeof(provider) == 'undefined' || !provider.config) { throw("No config found.") } const { zoneId } = provider.config; if (!zoneId) { throw("You must specify a Zone ID CLOUDFLARE_ZONE_ID"); } const response = await cf.routes.getRoutes({zoneId}); const { result } = response; // check for all the workers we are uploading const foundDuplicate = result.some(filters => { const { pattern, script } = filters; const functions = serverless.service.getAllFunctions(); for (const scriptName of functions) { const functionObject = serverless.service.getFunction(scriptName); const routes = functionObject.events.map(function(event) { if (event.http) { return event.http.url; } }) //let uploadedName = functionObject.name || scriptName; return routes.some(r => { return r === pattern && functionObject.name !== script; }); } }); return foundDuplicate; } } ================================================ FILE: shared/multiscript.js ================================================ /** * Copyright (c) 2018, Cloudflare. All rights reserved. * * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER * IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ const cf = require("cloudflare-workers-toolkit"); const path = require("path"); const { generateCode, generateWASM } = require("../deploy/lib/workerScript"); module.exports = { getRoutes(events) { return events.map(function (event) { if (event.http) { return event.http.url; } }); }, /** * Parses the resources and environment config to build bindings for the worker. async because it has to get namespaces for the CF id * @param {*} provider * @param {*} functionObject */ async getBindings(provider, functionObject) { let bindings = []; let resources = functionObject.resources; if (resources && resources.kv) { // do nothing if there is no kv config const namespaces = await cf.storage.getNamespaces(); let namespaceBindings = resources.kv.map(function (store) { return { name: store.variable, type: 'kv_namespace', namespace_id: namespaces.find(function (ns) { return ns.title === store.namespace; }).id } }); bindings = bindings.concat(namespaceBindings); } if (resources && resources.wasm) { let wasmBindings = resources.wasm.map(function(wasm) { return { name: wasm.variable, type: 'wasm_module', part: path.basename(wasm.file, path.extname(wasm.file)) } }); bindings = bindings.concat(wasmBindings); } // Get Environment Variables let envVars = Object.assign({}, provider.environment); envVars = Object.assign(envVars, functionObject.environment); for (const key in envVars) { bindings.push({ name: key, type: 'secret_text', text: envVars[key] }); } return bindings; }, /** * Deploys the Worker Script in functionObject from the yml file * @param {*} accountId * @param {*} service * @param {*} functionObject */ async deployWorker(accountId, serverless, functionObject) { const { service } = serverless; cf.setAccountId(accountId); const contents = generateCode(serverless, functionObject); let bindings = await this.getBindings(service.provider, functionObject); let t = await cf.workers.deploy({ accountId, name: functionObject.name, script: contents, wasm: generateWASM(functionObject), bindings }) return t; }, /** * Deploys the namespaces in function Object listed under resources->storage * @param {*} accountId * @param {*} functionObject */ async deployNamespaces(accountId, functionObject) { let responses = []; if (functionObject.resources && functionObject.resources.kv) { for (const store of functionObject.resources.kv) { let result = await cf.storage.createNamespace({ accountId, name: store.namespace }); if (cf.storage.isDuplicateNamespaceError(result)) { result.success = true; } responses.push(result); } } return responses; }, /** * Deploys all routes found in functionObject.events * @param {*} zoneId * @param {*} functionObject */ async deployRoutes(zoneId, functionObject) { const allRoutes = this.getRoutes(functionObject.events); let routeResponses = []; for (const pattern of allRoutes) { const response = await cf.routes.deploy({ path: pattern, scriptName: functionObject.name, zoneId }); routeResponses.push(response) } return routeResponses; } } ================================================ FILE: shared/validate.js ================================================ /** * Copyright (c) 2018, Cloudflare. All rights reserved. * * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER * IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ const BB = require("bluebird"); module.exports = { validateFunctionName() { return BB.bind(this) .then(this.checkIfFuntionParamPresent) .then(this.checkFunctionName); }, checkIfFuntionParamPresent() { let funParam = this.options.function; if (funParam === undefined) { funParam = this.options.f; } if (funParam === undefined || funParam === null || funParam === "") { return BB.reject("Invoke function with -f or --function"); } return funParam; }, /** * Ensures that funParam is an array of functions and ensures there are config entries for each. */ checkFunctionName(funParam) { const functions = this.serverless.service.getAllFunctions(); if (!Array.isArray(funParam)) { funParam = Array(funParam) } for (let func of funParam) { if (functions.indexOf(func) === -1) { return BB.reject(`The specified function: ${func} was not found in your configuration file.`); } } return funParam; }, isValidScriptName(sname) { const re = new RegExp("^[a-z0-9_][A-Za-z0-9-_]*$"); if (re.exec(sname)) { return true; } return false; }, getInvalidScriptNames() { const functions = this.serverless.service.getAllFunctions(); const notValidScriptNames = functions.find(scriptName => { return !this.isValidScriptName(scriptName); }); return notValidScriptNames; } }; ================================================ FILE: test/shared/duplicate_test.js ================================================ const proxyquire = require('proxyquire') const assert = require('assert'); const validate = proxyquire("../../shared/duplicate", { 'cloudflare-workers-toolkit': { "routes": { 'getRoutes': function() { return Promise.resolve({ "result": [{ pattern: "route1", script: "existing" }] }) } } } }); describe("checkIfDuplicateRoutes", function() { it("should fail gracefully if no config is provided", async function() { try { await validate.checkIfDuplicateRoutes(); } catch (e) { assert.equal(e, "No config found."); } }); const PROVIDER = { config: { accountId: 12, zoneId: 13 } } const FUNCTIONS = ["first", "second"]; const SERVERLESS = { service: { getAllFunctions: function() { return FUNCTIONS; }, getFunction: function(name) { return { webpack: false, name: name, script: `test/handlers/${name}`, events: [ { http: {url: "route1", method: 'GET'}} ] } } } } it("should detect duplicates in the same yml file", async function() { const result = await validate.checkIfDuplicateRoutes(SERVERLESS, PROVIDER); assert.equal(result, true) }); }); ================================================ FILE: test/shared/multiscript_test.js ================================================ const proxyquire = require('proxyquire') const assert = require('assert'); const ms = proxyquire("../../shared/multiscript", { "cloudflare-workers-toolkit": { } }); const EVENTS = [{ 'http': { url: 'somedomain.com/route1', method: 'GET' } }, { 'http': { url: 'somedomain.com/route2', method: 'GET' } }]; describe("getRoutes", function() { it("pulls routes out of the event config", function() { const routes = ms.getRoutes(EVENTS); assert.deepEqual(routes, ['somedomain.com/route1', 'somedomain.com/route2']) }); }); ================================================ FILE: utils/index.js ================================================ /** * Copyright (c) 2018, Cloudflare. All rights reserved. * * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER * IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ module.exports = { getFunctionObject(paramName) { let funParam = paramName || this.options.function; if (typeof funParam === "undefined") { funParam = this.options.f; } if (funParam) { return this.serverless.service.getFunction(funParam); } else return null; }, getFunctionObjectForSingleScript() { const [functionName] = this.serverless.service.getAllFunctions(); return this.getFunctionObject(functionName); }, parseWorkerResponse(serverlessConsole, apiResponse) { let { success: workerDeploySuccess, result: workerResult, errors: workerErrors } = apiResponse; const { id, size } = workerResult || {}; if (workerDeploySuccess) { serverlessConsole.log( `✅ Script Deployed. Name: ${id}, Size: ${(size / 1024).toFixed(2)}K` ); } else { serverlessConsole.log(`❌ Fatal Error, Script Not Deployed!`); workerErrors.forEach(err => { let { code, message } = err; serverlessConsole.log( `--> Error Code:${code}\n--> Error Message: "${message}"` ); }); } return { workerDeploySuccess, workerResult, workerErrors }; }, aggregateWorkerResponse(serverlessConsole, apiResponse) { let status = []; apiResponse.forEach(resp => { status.push(this.parseWorkerResponse(serverlessConsole, resp)); }); return status; }, parseRoutesResponse(serverlessConsole, apiResponse) { let status = []; apiResponse.forEach(resp => { let { success: routeSuccess, result: routeResult, errors: routeErrors } = resp; if (routeSuccess || !this.routeContainsFatalErrors(routeErrors)) { serverlessConsole.log(`✅ Routes Deployed `); } else { serverlessConsole.log(`❌ Fatal Error, Routes Not Deployed!`); routeErrors.forEach(err => { let { code, message } = err; serverlessConsole.log( `--> Error Code:${code}\n--> Error Message: "${message}"` ); }); } status.push({ routeSuccess, routeResult, routeErrors }); }); return status; }, aggregateRoutesResponse(serverlessConsole, apiResponse) { let status = []; apiResponse.forEach(resp => { status.push(this.parseRoutesResponse(serverlessConsole, resp)); }); return status; }, routeContainsFatalErrors(errors) { // suppress 10020 duplicate routes error // no need to show error when they are simply updating their script return errors.some(e => e.code !== 10020); } }; ================================================ FILE: utils/webpack.js ================================================ const path = require("path"); const BB = require("bluebird"); const webpack = BB.promisify(require("webpack")); const fse = require("fs-extra"); function getDefaultWebPackConfig(serverless, functionObject) { const outputPath = path.join( serverless.config.servicePath, functionObject.script ); return { config: { entry: { out: outputPath }, output: { filename: `${functionObject.script}.js`, path: path.join(serverless.config.servicePath, "dist") }, devtool: "cheap-module-source-map", target: "webworker", mode: "production" }, outputPath }; } async function build(config) { try { let result = await webpack(config); let errors = result.compilation.errors; if (Array.isArray(errors) && errors) { errors.forEach(error => { console.log(`Webpack Error: ${error}`); }); } } catch (error) { // failed to webpack console.log(`Webpack Error: ${error}`); } } module.exports = { pack: async function (serverless, functionObject, webpackConfigFile = null) { serverless.cli.log(`bundling: ${functionObject.script}`); const startTime = Date.now() let config = null, outputPath = ''; //If webpack config file is provided use that if (webpackConfigFile && typeof webpackConfigFile === 'string') { console.log(`Building web pack with config ${webpackConfigFile}.js`) config = require(path.resolve("./" + webpackConfigFile)); } else if (typeof (functionObject.webpack) === 'boolean' && functionObject.webpack) { //webpack is set to true in function object. Let's generate bundle for just this function console.log(`Building web pack with config default config for ${functionObject.script}`); ({ config, outputPath } = getDefaultWebPackConfig(serverless, functionObject)); } else if (typeof (functionObject.webpack) === 'string' && functionObject.webpack) { //function has individual webpack config. Build that console.log(`Building web pack with config file ${functionObject.webpack} for ${functionObject.script}`); ({ config } = getDefaultWebPackConfig(serverless, functionObject)); config = Object.assign(config, require(path.resolve("./" + functionObject.webpack))); } if (!config) { //No config found, probably did a global webpack build. Exit return } await build(config) console.log(`Webpack finished in ${(Date.now() - startTime) / 1000} seconds`) if (outputPath) { fse.removeSync(outputPath); } }, packGlobalWebpack: async function (serverless) { // Let's see if there is global webpack that we need to build const globalWebPackConfig = serverless.service.serviceObject.webpackConfig; if (globalWebPackConfig && typeof globalWebPackConfig === 'string') { await this.pack(serverless, { script: 'Global web pack config' }, globalWebPackConfig); } }, getAssetPathPrefix: function (serverless, functionObject) { // If web pack is used and not explicitly turned off for this script, append dist in-front of the path return (serverless.service.serviceObject.webpackConfig || functionObject.webpack) && functionObject.webpack !== false ? './dist/' : ''; } };