├── .github └── workflows │ └── npm-publish.yml ├── .gitignore ├── LICENSE ├── README.md ├── docs ├── .gitignore ├── README.md ├── babel.config.js ├── blog │ ├── 2019-05-28-hola.md │ ├── 2019-05-29-hello-world.md │ └── 2019-05-30-welcome.md ├── docs │ ├── cookies.md │ ├── doc3.md │ ├── install.md │ ├── mdx.md │ ├── response.md │ ├── stopwatch.md │ └── websockets.md ├── docusaurus.config.js ├── package-lock.json ├── package.json ├── sidebars.js ├── src │ ├── css │ │ └── custom.css │ └── pages │ │ ├── index.js │ │ └── styles.module.css └── static │ ├── .nojekyll │ └── img │ ├── favicon.ico │ ├── logo.svg │ ├── undraw_docusaurus_mountain.svg │ ├── undraw_docusaurus_react.svg │ └── undraw_docusaurus_tree.svg ├── example.js ├── example.webpack.config.js ├── example.websocket.js ├── examples └── socketio.js ├── package-lock.json ├── package.json ├── postBuild.js ├── rollup.config.js ├── src ├── cookies.js ├── index.js ├── secrets.js ├── stopwatch.js └── websockets.js └── wrangler.toml /.github/workflows/npm-publish.yml: -------------------------------------------------------------------------------- 1 | on: push 2 | 3 | jobs: 4 | publish: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - uses: actions/checkout@v1 8 | - uses: actions/setup-node@v1 9 | with: 10 | node-version: 12 11 | - run: npm install 12 | - run: npm run build 13 | - uses: JS-DevTools/npm-publish@v1 14 | with: 15 | token: ${{ secrets.NPM_TOKEN }} 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | worker 10 | 11 | # Diagnostic reports (https://nodejs.org/api/report.html) 12 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 13 | 14 | # Runtime data 15 | pids 16 | *.pid 17 | *.seed 18 | *.pid.lock 19 | 20 | # Directory for instrumented libs generated by jscoverage/JSCover 21 | lib-cov 22 | 23 | # Coverage directory used by tools like istanbul 24 | coverage 25 | *.lcov 26 | 27 | # nyc test coverage 28 | .nyc_output 29 | 30 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 31 | .grunt 32 | 33 | # Bower dependency directory (https://bower.io/) 34 | bower_components 35 | 36 | # node-waf configuration 37 | .lock-wscript 38 | 39 | # Compiled binary addons (https://nodejs.org/api/addons.html) 40 | build/Release 41 | 42 | # Dependency directories 43 | node_modules/ 44 | jspm_packages/ 45 | 46 | # Snowpack dependency directory (https://snowpack.dev/) 47 | web_modules/ 48 | 49 | # TypeScript cache 50 | *.tsbuildinfo 51 | 52 | # Optional npm cache directory 53 | .npm 54 | 55 | # Optional eslint cache 56 | .eslintcache 57 | 58 | # Microbundle cache 59 | .rpt2_cache/ 60 | .rts2_cache_cjs/ 61 | .rts2_cache_es/ 62 | .rts2_cache_umd/ 63 | 64 | # Optional REPL history 65 | .node_repl_history 66 | 67 | # Output of 'npm pack' 68 | *.tgz 69 | 70 | # Yarn Integrity file 71 | .yarn-integrity 72 | 73 | # dotenv environment variables file 74 | .env 75 | .env.test 76 | 77 | # parcel-bundler cache (https://parceljs.org/) 78 | .cache 79 | .parcel-cache 80 | 81 | # Next.js build output 82 | .next 83 | out 84 | 85 | # Nuxt.js build / generate output 86 | .nuxt 87 | dist 88 | 89 | # Gatsby files 90 | .cache/ 91 | # Comment in the public line in if your project uses Gatsby and not Next.js 92 | # https://nextjs.org/blog/next-9-1#public-directory-support 93 | # public 94 | 95 | # vuepress build output 96 | .vuepress/dist 97 | 98 | # Serverless directories 99 | .serverless/ 100 | 101 | # FuseBox cache 102 | .fusebox/ 103 | 104 | # DynamoDB Local files 105 | .dynamodb/ 106 | 107 | # TernJS port file 108 | .tern-port 109 | 110 | # Stores VSCode versions used for testing VSCode extensions 111 | .vscode-test 112 | 113 | # yarn v2 114 | .yarn/cache 115 | .yarn/unplugged 116 | .yarn/build-state.yml 117 | .yarn/install-state.gz 118 | .pnp.* 119 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Connor Vince 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🦄 CF Workers: easy-utils 2 | *A library designed to make writing workers so much cleaner and easier!* 3 | 4 | Welcome to easy-utils, this library is designed to make writing Workers easy as heck. Included in this package is a bunch of helpers for responses, profiling, handling cookies, and serving any static assets. Now with full Websocket support! 5 | 6 | Turn your Worker code from spaghetti hell to majestic artwork today with easy-utils. 7 | 8 | **Documentation**: 9 | ## https://easy-utils.docs.ceru.dev/docs/ 10 | 11 | ## 🔧 Install 12 | `npm i cfw-easy-utils` 13 | 14 | *Note: this lib has tree-shaking enabled by default. Only import what you need and you will keep your package size low!* 15 | 16 | ## ✨ Examples 17 | 18 | ### 🔨 No more JSON.stringify! 19 | Stuck having to copy paste the same 4 lines of code just to return a simple JSON object? Now you can easily return any JSON-compatible object/array without having to worry about your code getting complex. 20 | ```js 21 | return response.json({ 'hello': 'world!' }) 22 | ``` 23 | 24 | ### 🎨 Cache S3 buckets & more! 25 | Want to save money on your bill? Cache your assets on the edge and speed up your images. All of Cloudflare's caching power in a single line of code. 26 | ```js 27 | return response.static(request, { baseUrl: 'https://yourbucket.net' }) 28 | ``` 29 | 30 | ### 🔌 Go realtime with Websockets! 31 | Support Websockets at the edge with our util library. Get access to a full Websocket client and server for your every need. 32 | ```js 33 | import { response, Websocket, WebsocketResponse } from 'cfw-easy-utils' 34 | 35 | var ws = new Websocket('ws://echo.websocket.org') // Client 36 | var resp = new WebsocketResponse() // Server 37 | 38 | ws.on('message', (msg) => { 39 | resp.send(msg) 40 | }) 41 | 42 | resp.on('message', (msg) => { 43 | ws.send(msg) 44 | }) 45 | 46 | return response.websocket(resp) 47 | ``` 48 | 49 | ### 🍃 Handle CORS with a breeze! 50 | CORS can be so cumbersome and annoying. Let easy-utils handle it for you so you can focus on your Worker's real purpose. 51 | ```js 52 | if (request.method == 'OPTIONS') { 53 | return response.cors() 54 | } 55 | 56 | // autoCors is true by default, but we want to show it off. 57 | return response.json({ hello: 'World' }, { autoCors: true }) 58 | ``` 59 | 60 | ### ⏱ Time your I/O to reduce slowdowns. 61 | Record your times to watch for any slow I/O events. Will only count I/O as Workers cannot count CPU time internally. All you need to do is mark some times, then set the Stopwatch option on *any* of easy-utils response handler and it will set the `Server-Timing` header for you. Look in the `Timing` header of your modern browser to view the timings. 62 | ```js 63 | import { Stopwatch } from 'cfw-easy-utils' 64 | 65 | const watch = new Stopwatch() 66 | const value = await KVNAMESPACE.get('somekey') 67 | watch.mark('Got KV response') 68 | return response.json({ kv: value }, { stopwatch: watch }) 69 | ``` 70 | 71 | ### 😄 Generate user accounts on the edge 72 | Want to take your user experience to the next level? You can generate password hashes and UUID's direct in your Worker. 73 | ```js 74 | import { response, secrets } from 'cfw-easy-utils' 75 | 76 | // Later, use secrets.verifyPassword to verify someone's identity. 77 | return response.json({ 78 | id: secrets.uuidv4(), 79 | passwordHash: await secrets.hashPassword('passw0rd01') 80 | }) 81 | ``` 82 | 83 | ### 🦄 This is only some of the examples of what you can do with cfw-easy-utils. 84 | 85 | Made by Connor Vince with the help and love from the Cloudflare Worker's community. Thank you everyone! -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | /node_modules 3 | 4 | # Production 5 | /build 6 | 7 | # Generated files 8 | .docusaurus 9 | .cache-loader 10 | 11 | # Misc 12 | .DS_Store 13 | .env.local 14 | .env.development.local 15 | .env.test.local 16 | .env.production.local 17 | 18 | npm-debug.log* 19 | yarn-debug.log* 20 | yarn-error.log* 21 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Easy utils documentation website 2 | ## 🦄 [easy-utils.docs.ceru.dev](https://easy-utils.docs.ceru.dev) 3 | 4 | This website is built using [Docusaurus 2](https://v2.docusaurus.io/), a modern static website generator. 5 | 6 | ## Installation 7 | 8 | ```console 9 | yarn install 10 | ``` 11 | 12 | ## Local Development 13 | 14 | ```console 15 | yarn start 16 | ``` 17 | 18 | This command starts a local development server and open up a browser window. Most changes are reflected live without having to restart the server. 19 | 20 | ## Build 21 | 22 | ```console 23 | yarn build 24 | ``` 25 | 26 | This command generates static content into the `build` directory and can be served using any static contents hosting service. 27 | 28 | ## Deployment 29 | 30 | ```console 31 | GIT_USER= USE_SSH=true yarn deploy 32 | ``` 33 | 34 | If you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch. 35 | -------------------------------------------------------------------------------- /docs/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [require.resolve('@docusaurus/core/lib/babel/preset')], 3 | }; 4 | -------------------------------------------------------------------------------- /docs/blog/2019-05-28-hola.md: -------------------------------------------------------------------------------- 1 | --- 2 | slug: hola 3 | title: Hola 4 | author: Gao Wei 5 | author_title: Docusaurus Core Team 6 | author_url: https://github.com/wgao19 7 | author_image_url: https://avatars1.githubusercontent.com/u/2055384?v=4 8 | tags: [hola, docusaurus] 9 | --- 10 | 11 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet 12 | -------------------------------------------------------------------------------- /docs/blog/2019-05-29-hello-world.md: -------------------------------------------------------------------------------- 1 | --- 2 | slug: hello-world 3 | title: Hello 4 | author: Endilie Yacop Sucipto 5 | author_title: Maintainer of Docusaurus 6 | author_url: https://github.com/endiliey 7 | author_image_url: https://avatars1.githubusercontent.com/u/17883920?s=460&v=4 8 | tags: [hello, docusaurus] 9 | --- 10 | 11 | Welcome to this blog. This blog is created with [**Docusaurus 2 alpha**](https://v2.docusaurus.io/). 12 | 13 | 14 | 15 | This is a test post. 16 | 17 | A whole bunch of other information. 18 | -------------------------------------------------------------------------------- /docs/blog/2019-05-30-welcome.md: -------------------------------------------------------------------------------- 1 | --- 2 | slug: welcome 3 | title: Welcome 4 | author: Yangshun Tay 5 | author_title: Front End Engineer @ Facebook 6 | author_url: https://github.com/yangshun 7 | author_image_url: https://avatars0.githubusercontent.com/u/1315101?s=400&v=4 8 | tags: [facebook, hello, docusaurus] 9 | --- 10 | 11 | Blog features are powered by the blog plugin. Simply add files to the `blog` directory. It supports tags as well! 12 | 13 | Delete the whole directory if you don't want the blog features. As simple as that! 14 | -------------------------------------------------------------------------------- /docs/docs/cookies.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: cookies 3 | title: 🍪 Cookie utilities 4 | --- 5 | 6 | This section of the package focuses on cookies and how to integrate them with your Worker. 7 | 8 | 9 | ```js title="Example" 10 | import { response, cookie } from 'cfw-easy-utils' 11 | 12 | // you don't need a request to make this work but its needed if you want to read 13 | // existing cookies. 14 | var cookieJar = cookie.jar(request) 15 | 16 | var token = cookies.get('authToken') 17 | var user = JSON.parse(cookieJar.get('user')) 18 | 19 | if (!token) { 20 | cookieJar.set('authToken', 'tokenvalue') // accepts any string value 21 | cookieJar.set('user', JSON.stringify({ id: 1, username: 'Cerulean' })) 22 | 23 | // all of easy-util's response helpers have native support for cookies, 24 | // you just need to provide a cookie jar object. 25 | return response.json({ hello: world }, { cookies: cookieJar }) 26 | } 27 | 28 | return response.text(`You are logged in with this token: "${cookieJar.get('authToken')}"`) 29 | ``` 30 | 31 | :::warning 32 | 33 | Be aware that cookie.jar will *not* set existing cookies when doing `setHeaders`. Browsers will keep cookies until they expire so we felt this was un-necessary to keep setting existing cookies. 34 | 35 | ::: 36 | 37 | --- 38 | 39 | ### cookie.jar(request: Request or Null) 40 | Returns a new CookieJar object. This allows you to read the request's cookies or set your own. Be aware that cookies read from the request wont be saved by default. If you want to send the cookies that were sent with the request back, you need to iterate over the cookies and add them to the CookieJar. 41 | 42 | ```js title="Example" 43 | import { cookie } from 'cfw-easy-utils' 44 | 45 | // inside the request handler 46 | var cookieJar = cookie.jar(request) 47 | ``` 48 | 49 | ### CookieJar.get(key: String) 50 | Returns the cookies value, or it will return an empty string if no value was found. 51 | 52 | ### CookieJar.set(key: String, value: String, options: Object) 53 | Sets the cookie value. Returns `null`. 54 | 55 | The options object accepts values for the cookie. For example, you can set the secure mode via { secure: true }. 56 | 57 | :::info 58 | 59 | If you want to append your cookies to a response, you can slot the CookieJar object into any of easy-util's response helpers. 60 | 61 | ```js 62 | return response.json({}, { cookies: cookieJar }) 63 | ``` 64 | 65 | ::: -------------------------------------------------------------------------------- /docs/docs/doc3.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: doc3 3 | title: This is Document Number 3 4 | --- 5 | 6 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. In ac euismod odio, eu consequat dui. Nullam molestie consectetur risus id imperdiet. Proin sodales ornare turpis, non mollis massa ultricies id. Nam at nibh scelerisque, feugiat ante non, dapibus tortor. Vivamus volutpat diam quis tellus elementum bibendum. Praesent semper gravida velit quis aliquam. Etiam in cursus neque. Nam lectus ligula, malesuada et mauris a, bibendum faucibus mi. Phasellus ut interdum felis. Phasellus in odio pulvinar, porttitor urna eget, fringilla lectus. Aliquam sollicitudin est eros. Mauris consectetur quam vitae mauris interdum hendrerit. Lorem ipsum dolor sit amet, consectetur adipiscing elit. 7 | 8 | Duis et egestas libero, imperdiet faucibus ipsum. Sed posuere eget urna vel feugiat. Vivamus a arcu sagittis, fermentum urna dapibus, congue lectus. Fusce vulputate porttitor nisl, ac cursus elit volutpat vitae. Nullam vitae ipsum egestas, convallis quam non, porta nibh. Morbi gravida erat nec neque bibendum, eu pellentesque velit posuere. Fusce aliquam erat eu massa eleifend tristique. 9 | 10 | Sed consequat sollicitudin ipsum eget tempus. Integer a aliquet velit. In justo nibh, pellentesque non suscipit eget, gravida vel lacus. Donec odio ante, malesuada in massa quis, pharetra tristique ligula. Donec eros est, tristique eget finibus quis, semper non nisl. Vivamus et elit nec enim ornare placerat. Sed posuere odio a elit cursus sagittis. 11 | 12 | Phasellus feugiat purus eu tortor ultrices finibus. Ut libero nibh, lobortis et libero nec, dapibus posuere eros. Sed sagittis euismod justo at consectetur. Nulla finibus libero placerat, cursus sapien at, eleifend ligula. Vivamus elit nisl, hendrerit ac nibh eu, ultrices tempus dui. Nam tellus neque, commodo non rhoncus eu, gravida in risus. Nullam id iaculis tortor. 13 | 14 | Nullam at odio in sem varius tempor sit amet vel lorem. Etiam eu hendrerit nisl. Fusce nibh mauris, vulputate sit amet ex vitae, congue rhoncus nisl. Sed eget tellus purus. Nullam tempus commodo erat ut tristique. Cras accumsan massa sit amet justo consequat eleifend. Integer scelerisque vitae tellus id consectetur. 15 | -------------------------------------------------------------------------------- /docs/docs/install.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: install 3 | title: Installation 4 | sidebar_label: Installation 5 | slug: / 6 | --- 7 | 8 | Welcome to easy-utils, the best helper package for Cloudflare Workers! Get started with `cfw-easy-utils` using this guide. 9 | 10 | ### Why? 11 | easy-utils was created by Cerulean to ease developer stress and reduce code clutter, making developing with easy-utils a must. Instead of manually calling `JSON.stringify` and setting the correct Content-Type headers, `response.json` makes it ultra simple to return JSON from your worker. This is just one example of how easy-utils can help you. It also handles CORS for you so you dont have to inject CORS headers into every response. 12 | 13 | Responses are not all that easy-utils can do. We have helper functions for a bunch of common tasks with Workers. 14 | 15 | :::info 16 | While this package is focused on Cloudflare Workers, this package can run in any environment that has Request or Response objects built into JS. 17 | ::: 18 | ### NPM 19 | `npm i cfw-easy-utils@latest` 20 | 21 | ### Wrangler template 22 | Installs a basic Worker template along side easy-utils. 23 | 24 | `wrangler generate projectname https://github.com/aggressivelymeows/cfw-easy-utils-template` 25 | 26 | -------------------------------------------------------------------------------- /docs/docs/mdx.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: mdx 3 | title: Powered by MDX 4 | --- 5 | 6 | You can write JSX and use React components within your Markdown thanks to [MDX](https://mdxjs.com/). 7 | 8 | export const Highlight = ({children, color}) => ( {children} ); 14 | 15 | Docusaurus green and Facebook blue are my favorite colors. 16 | 17 | I can write **Markdown** alongside my _JSX_! 18 | -------------------------------------------------------------------------------- /docs/docs/response.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: response 3 | title: 📦 Response utilities 4 | --- 5 | 6 | This is a collection of helpers to make responding to requests easier. The core idea is to reduce errors, and make your code easier to read by moving a lot of the repeated code into its own package. 7 | 8 | ```js title="Before - ❌ Messy, hard to change if theres an issue." 9 | return new Response( 10 | JSON.stringify({ 'hello': 'world' }), 11 | { headers: { 'content-type': 'application/json' } }, 12 | ) 13 | ``` 14 | 15 | ```js title="After - 🦄 Much cleaner." 16 | return response.json({ 'hello': 'world!' }) 17 | ``` 18 | 19 | --- 20 | 21 | ## response.static 22 | ***response.static(request: Request, options: Object)*** 23 | 24 | Returns your static assets to the Worker while adding asset to Cloudflare's cache. You must specify `baseUrl` as the root url you want all of the asset requests to be sent too. This can be any asset serving url such as S3 or DigitalOcean: Spaces. 25 | 26 | #### Options 27 | **baseUrl: String** 28 | The base URL of the resource you want to fetch. For example: 'https://example.com'. Make sure you allow public reads from this bucket otherwise the Worker wont be able to fetch the resource. 29 | 30 | **ttl: Integer** 31 | The time in seconds for how long you wish to keep this asset cached on Cloudflare's servers. Defaults to `600` seconds (10 minutes). 32 | 33 | **routePrefix: String** 34 | If you are running this behind a route, you might want to use this to remove the route's prefix. For example, if you had this running behind `/cdn` you might want to remove `/cdn` from the asset path otherwise it might not find the asset you want. This will remove the string you pass from the Request path. 35 | 36 | ```js title="Example" 37 | return response.static(request, { baseUrl: 'https://example.com', ttl: 1600, routePrefix: '/cdn' }) 38 | ``` 39 | 40 | ## response.json 41 | ***response.json(body: StringOrObject, options: OptionsObject)*** 42 | 43 | Returns a response object ready to be returned to the client with correct headers set. Accepts either a plain JS object or an already serialized JSON string. If the body is anything other than an Object, easy-utils will just send that as the response and will not convert it for you. 44 | 45 | ```js 46 | return response.json({ 'hello': 'world' }) 47 | ``` 48 | 49 | ## response.html 50 | ***response.html(body: String, options: OptionsObject)*** 51 | 52 | Turns the body param into a valid HTML response. Does not sanitize HTML and will not validate for structure. 53 | 54 | ```js 55 | return response.html('

hello, world!

') 56 | ``` 57 | 58 | ## response.cors 59 | ***response.cors()*** 60 | 61 | Returns `null` but with CORS headers set as if it were an `OPTIONS` request. 62 | You might be interested in this snippet for handling CORS requests: 63 | 64 | ```js 65 | addEventListener('fetch', (event) => { 66 | if (event.request.method == 'OPTIONS') { 67 | return event.respondWith(response.cors()) 68 | } 69 | 70 | return event.respondWith(handleRequest(event.request)); 71 | }) 72 | ``` 73 | 74 | --- 75 | #### OptionsObject 76 | An object describing what extras to modify the response with. Almost all of the response functions accept this as the second argument except where stated. 77 | 78 | ##### headers, default: `{}` 79 | An object for setting the response headers. Adding headers this way will overwrite any existing headers that easy-utils sets. 80 | 81 | ##### status, default: `200` 82 | The status of the response. Defaults to 200, status message will be ignored by Cloudflare's CDN but we support setting it for other environments. 83 | 84 | ##### autoCors, default: `true` 85 | Adds standard CORS headers to the response so you don't have to deal with CORS errors. To set the default values, set the values on the response module you imported: 86 | 87 | ```js title="Set origin globally" 88 | import { response } from 'cfw-easy-utils' 89 | response.accessControl.allowOrigin = 'https://example.com' 90 | ``` 91 | 92 | ```js title="Make easy-utils 'Origin-Aware'" 93 | import { response } from 'cfw-easy-utils' 94 | // setting .request means easy-utils will try to use the 95 | // origin of the inbound request for its CORS data. 96 | response.request = request 97 | return response.json() 98 | ``` 99 | 100 | ##### cookies, default: `null` 101 | If present, must be an instance of easy-utils's CookieJar. Please read documentation on CookieJar for more information. 102 | 103 | ##### stopwatch, default: `null` 104 | If present, must be an instance of easy-utils's Stopwatch. Please read documentation on Stopwatch for more information. 105 | 106 | --- 107 | 108 | ## 💎 Header utilities 109 | ## response.setHeaders 110 | ***response.setHeaders(response: Response, name: String, value: String)*** 111 | 112 | Returns a **new** Response object with the headers set. Due to how JS handles responses, its safer to create a new Response object instead of fail to modify an immutable Response. 113 | 114 | ## response.headersToObject 115 | ***response.headersToObject(headers: Headers)*** 116 | 117 | Takes an Headers object and turns it into a plain JS object. Repeated headers 118 | 119 | ```js title="Example" 120 | var headers = response.headersToObject(request.headers) 121 | ``` 122 | -------------------------------------------------------------------------------- /docs/docs/stopwatch.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: stopwatch 3 | title: ⌚ Stopwatch 4 | --- 5 | 6 | Time your code and monitor slow downs with external services! 7 | 8 | ### Heads up 9 | CF Workers cannot count their own CPU time however they are allowed to count I/O time. This library is designed to assist with monitoring fetch requests and other I/O such as Database requests. 10 | 11 | ```js title="Example" 12 | import { Stopwatch } from 'cfw-easy-utils' 13 | 14 | var watch = new Stopwatch() 15 | var resp = await fetch('https://example.com/') 16 | 17 | watch.mark('Fetched API resources') 18 | 19 | // inside the Server-Timing header, you can find the duration. 20 | // Most modern browsers also show this information in the "Timing" tab. 21 | return response.fromResponse(resp, { stopwatch: watch }) 22 | ``` 23 | 24 | ### Why? 25 | *"If CF Workers cant count CPU time, whats the point?" ~ Random user* 26 | The point of Stopwatch is to break down your internal requests for debugging. If you rely on an external service and it slows down, you can monitor using this library. Maybe in the future, I can add a utility to save these in KV so you can view them in a dashboard. -------------------------------------------------------------------------------- /docs/docs/websockets.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: websockets 3 | title: 🔌 Websocket utilities 4 | --- 5 | 6 | Want to run a Websocket server OR client at the edge? Now you can with the ease of use you love with cfw-easy-utils! 7 | 8 | ```js title="Client and server example" 9 | import { response, WebsocketResponse, Websocket } from 'cfw-easy-utils' 10 | 11 | // This example is a simple Websocket proxy that intercepts 12 | // and processes the messages at the edge. 13 | var resp = new WebsocketResponse() 14 | var ws = new Websocket('ws://echo.websocket.org') 15 | 16 | ws.on('message', (msg) => { 17 | // data coming from the external service 18 | resp.send(msg) 19 | }) 20 | 21 | resp.on('message', (msg) => { 22 | // data coming from the end user 23 | ws.send(msg) 24 | }) 25 | 26 | return response.websocket(resp) 27 | ``` 28 | 29 | --- 30 | 31 | ## Websocket 32 | ***new Websocket(url)*** 33 | 34 | Creates a new Websocket client. Works exactly like you would expect from a Websocket client however it hides a lot of the messy stuff you would have to deal with as a developer. 35 | 36 | ### on(event: String, callback: Function) 37 | 38 | Runs a function every time an event is triggered. Example events include `message`, `ready`, and `closed`. CFW-EU will try its best to decode the incoming event but if it fails for whatever reason, the full body will be sent to the callback. For example, if you are using a JSON Websocket server, we will try to parse but it is failsafe. 39 | 40 | :::info 41 | You can also use `addEventListener` if you want to keep compatibility with existing code. Be aware that addEventListener'ed events will **not** be processed by this lib. It is a straight passthrough. 42 | ::: 43 | 44 | ### send(body: [WebsocketValidBody](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/send#syntax)) 45 | 46 | Sends the body to the client. If the body is a plain JS Object or Array, it will be JSON'ifyed before being sent. Any other type of body will be left as is to be sent as a Binary frame. 47 | 48 | ```js title="Not part of the official standard but its available as a nice helper:" 49 | socket.send({ some: data }) 50 | ``` 51 | 52 | ## WebsocketResponse 53 | ***new WebsocketResponse()*** 54 | 55 | Returns easy-util's Websocket client. Designed to make it easier to create a Websocket server at the edge. 56 | 57 | ### on(event: String, callback: Function) 58 | 59 | Runs the callback function every time the event is fired. Check MDN for Websocket based events, they are effectively the same except for message. 60 | 61 | **message** - Fired when a new message is received by the Websocket client. The body is attempted to be converted to JSON but if it fails, it will just be sent to the client. 62 | 63 | ### send(body: [WebsocketValidBody](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/send#syntax)) 64 | 65 | Sends the body to the client. If the body is a plain JS Object or Array, it will be JSON'ifyed before being sent. Any other type of body will be left as is to be sent as a Binary frame. 66 | -------------------------------------------------------------------------------- /docs/docusaurus.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | title: 'CF Workers: Easy utils', 3 | tagline: 'The best helper package for CF Workers!', 4 | url: 'https://utils.docs.ceru.dev', 5 | baseUrl: '/', 6 | onBrokenLinks: 'warn', 7 | favicon: 'img/favicon.ico', 8 | organizationName: 'Sponsus', // Usually your GitHub org/user name. 9 | projectName: 'cfw-easy-utils', // Usually your repo name. 10 | themeConfig: { 11 | defaultMode: 'dark', 12 | disableSwitch: true, 13 | 14 | navbar: { 15 | title: 'CF-Workers: Easy Utils', 16 | logo: { 17 | alt: 'My Site Logo', 18 | src: 'https://media.spns.us/ext/easy-utils.png', 19 | }, 20 | items: [ 21 | { 22 | to: 'docs/', 23 | activeBasePath: 'docs', 24 | label: 'Docs', 25 | position: 'left', 26 | }, 27 | { 28 | href: 'https://github.com/aggressivelymeows/cfw-easy-utils', 29 | label: 'GitHub', 30 | position: 'right', 31 | }, 32 | ], 33 | }, 34 | footer: { 35 | style: 'dark', 36 | links: [ 37 | ], 38 | copyright: `Copyright © ${new Date().getFullYear()} Connor Vince.`, 39 | }, 40 | }, 41 | presets: [ 42 | [ 43 | '@docusaurus/preset-classic', 44 | { 45 | docs: { 46 | sidebarPath: require.resolve('./sidebars.js'), 47 | // Please change this to your repo. 48 | editUrl: 49 | 'https://github.com/aggressivelymeows/cfw-easy-utils/docs/', 50 | }, 51 | blog: { 52 | showReadingTime: true, 53 | // Please change this to your repo. 54 | editUrl: 55 | 'https://github.com/aggressivelymeows/cfw-easy-utils/docs/', 56 | }, 57 | theme: { 58 | customCss: require.resolve('./src/css/custom.css'), 59 | }, 60 | }, 61 | ], 62 | ], 63 | }; 64 | -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "docs", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "docusaurus": "docusaurus", 7 | "start": "docusaurus start --host 0.0.0.0", 8 | "build": "docusaurus build", 9 | "swizzle": "docusaurus swizzle", 10 | "deploy": "docusaurus deploy", 11 | "serve": "docusaurus serve", 12 | "clear": "docusaurus clear" 13 | }, 14 | "dependencies": { 15 | "@docusaurus/core": "2.0.0-alpha.66", 16 | "@docusaurus/preset-classic": "2.0.0-alpha.66", 17 | "@mdx-js/react": "^1.6.21", 18 | "clsx": "^1.1.1", 19 | "react": "^16.8.4", 20 | "react-dom": "^16.8.4" 21 | }, 22 | "browserslist": { 23 | "production": [ 24 | ">0.5%", 25 | "not dead", 26 | "not op_mini all" 27 | ], 28 | "development": [ 29 | "last 1 chrome version", 30 | "last 1 firefox version", 31 | "last 1 safari version" 32 | ] 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /docs/sidebars.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | someSidebar: { 3 | 'Getting Started': ['install'], 4 | 'Documentation': ['response', 'cookies', 'stopwatch', 'websockets'] 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /docs/src/css/custom.css: -------------------------------------------------------------------------------- 1 | /* stylelint-disable docusaurus/copyright-header */ 2 | /** 3 | * Any CSS included here will be global. The classic template 4 | * bundles Infima by default. Infima is a CSS framework designed to 5 | * work well for content-centric websites. 6 | */ 7 | 8 | /* You can override the default Infima variables here. */ 9 | :root { 10 | --ifm-color-primary: #3a86ff; 11 | --ifm-color-primary-dark: #1b73ff; 12 | --ifm-color-primary-darker: #0b69ff; 13 | --ifm-color-primary-darkest: #0055db; 14 | --ifm-color-primary-light: #5999ff; 15 | --ifm-color-primary-lighter: #69a3ff; 16 | --ifm-color-primary-lightest: #98c0ff; 17 | } 18 | 19 | .docusaurus-highlight-code-line { 20 | background-color: rgb(72, 77, 91); 21 | display: block; 22 | margin: 0 calc(-1 * var(--ifm-pre-padding)); 23 | padding: 0 var(--ifm-pre-padding); 24 | } 25 | -------------------------------------------------------------------------------- /docs/src/pages/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import clsx from 'clsx'; 3 | import Layout from '@theme/Layout'; 4 | import Link from '@docusaurus/Link'; 5 | import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; 6 | import useBaseUrl from '@docusaurus/useBaseUrl'; 7 | import styles from './styles.module.css'; 8 | 9 | const features = [ 10 | { 11 | title: 'Easy to Use', 12 | imageUrl: 'img/undraw_docusaurus_mountain.svg', 13 | description: ( 14 | <> 15 | Docusaurus was designed from the ground up to be easily installed and 16 | used to get your website up and running quickly. 17 | 18 | ), 19 | }, 20 | { 21 | title: 'Focus on What Matters', 22 | imageUrl: 'img/undraw_docusaurus_tree.svg', 23 | description: ( 24 | <> 25 | Docusaurus lets you focus on your docs, and we'll do the chores. Go 26 | ahead and move your docs into the docs directory. 27 | 28 | ), 29 | }, 30 | { 31 | title: 'Powered by React', 32 | imageUrl: 'img/undraw_docusaurus_react.svg', 33 | description: ( 34 | <> 35 | Extend or customize your website layout by reusing React. Docusaurus can 36 | be extended while reusing the same header and footer. 37 | 38 | ), 39 | }, 40 | ]; 41 | 42 | function Feature({imageUrl, title, description}) { 43 | const imgUrl = useBaseUrl(imageUrl); 44 | return ( 45 |
46 | {imgUrl && ( 47 |
48 | {title} 49 |
50 | )} 51 |

{title}

52 |

{description}

53 |
54 | ); 55 | } 56 | 57 | function Home() { 58 | const context = useDocusaurusContext(); 59 | const {siteConfig = {}} = context; 60 | return ( 61 | 64 |
65 |
66 |

{siteConfig.title}

67 |

{siteConfig.tagline}

68 |
69 | 75 | Get Started 76 | 77 |
78 |
79 |
80 |
81 | {features && features.length > 0 && ( 82 |
83 |
84 |
85 | {features.map((props, idx) => ( 86 | 87 | ))} 88 |
89 |
90 |
91 | )} 92 |
93 |
94 | ); 95 | } 96 | 97 | export default Home; 98 | -------------------------------------------------------------------------------- /docs/src/pages/styles.module.css: -------------------------------------------------------------------------------- 1 | /* stylelint-disable docusaurus/copyright-header */ 2 | 3 | /** 4 | * CSS files with the .module.css suffix will be treated as CSS modules 5 | * and scoped locally. 6 | */ 7 | 8 | .heroBanner { 9 | padding: 4rem 0; 10 | text-align: center; 11 | position: relative; 12 | overflow: hidden; 13 | } 14 | 15 | @media screen and (max-width: 966px) { 16 | .heroBanner { 17 | padding: 2rem; 18 | } 19 | } 20 | 21 | .buttons { 22 | display: flex; 23 | align-items: center; 24 | justify-content: center; 25 | } 26 | 27 | .features { 28 | display: flex; 29 | align-items: center; 30 | padding: 2rem 0; 31 | width: 100%; 32 | } 33 | 34 | .featureImage { 35 | height: 200px; 36 | width: 200px; 37 | } 38 | -------------------------------------------------------------------------------- /docs/static/.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AggressivelyMeows/cfw-easy-utils/970990b3f24d60343c3d92b2f6dd5ebd5cb8c2a6/docs/static/.nojekyll -------------------------------------------------------------------------------- /docs/static/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AggressivelyMeows/cfw-easy-utils/970990b3f24d60343c3d92b2f6dd5ebd5cb8c2a6/docs/static/img/favicon.ico -------------------------------------------------------------------------------- /docs/static/img/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/static/img/undraw_docusaurus_mountain.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | -------------------------------------------------------------------------------- /docs/static/img/undraw_docusaurus_react.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | -------------------------------------------------------------------------------- /docs/static/img/undraw_docusaurus_tree.svg: -------------------------------------------------------------------------------- 1 | docu_tree -------------------------------------------------------------------------------- /example.js: -------------------------------------------------------------------------------- 1 | import { $ } from 'cfw-easy-html' 2 | import { Router } from '@neriko/cloudflare-workers-router' 3 | import { response, cookies, secrets, Stopwatch, WebsocketResponse, Websocket } from './src/index.js' 4 | 5 | global.WebSocket = Websocket 6 | 7 | import { io } from "socket.io-client" 8 | 9 | const d3 = require('d3-scale-chromatic') 10 | 11 | const router = new Router() 12 | 13 | router.get('/json', async (req) => { 14 | return response.json({ 'Hello': 'world!' }) 15 | }) 16 | 17 | router.get('/colour/:type/:value', async (req, params) => { 18 | var c = d3[params.type](params.value).replace('rgb(', '').replace(')', '') 19 | return response.json({ r: parseInt(c.split(',')[0].trim()), g: parseInt(c.split(',')[1].trim()), b: parseInt(c.split(',')[2].trim()) }) 20 | }) 21 | 22 | router.get('/ws', async (req, params) => { 23 | var resp = new WebsocketResponse() 24 | // Logger is an internal logging util such as an output Websocket. 25 | const socket = io("wss://socketio-chat-h9jt.herokuapp.com/socket.io", { 26 | reconnectionDelayMax: 10000, 27 | transports: ['websocket'], 28 | upgrade: false 29 | }) 30 | 31 | socket.on("connect", (_socket) => { 32 | resp.send("Client connected to server: " + clientName) 33 | resp.send("Transport being used: " + socket.io.engine.transport.name) 34 | }); 35 | 36 | resp.on('message', (msg) => { 37 | ws.send(msg) 38 | }) 39 | 40 | console.log(socket) 41 | 42 | return response.websocket(resp) 43 | }) 44 | 45 | router.get('/cdn/:path', async (req, params) => { 46 | // Serve content from a static host, in this example, my personal DigitalOcean S3-like host. 47 | // Because the route starts with /cdn, I want to remove that prefix from the asset location 48 | // otherwise it will try to find /cdn/static/2021/01/test.png which is 404. 49 | console.log('woof') 50 | return response.static(req, { 51 | baseUrl: 'https://cerulean.nyc3.digitaloceanspaces.com', 52 | routePrefix: '/cdn' 53 | }) 54 | }) 55 | 56 | router.get('/json/:message', async (req, params) => { 57 | // if we want our CORS headers to be origin-aware, we need to provide the response object with our original request. 58 | response.request = req 59 | 60 | return response.json( 61 | {'message': params.message}, 62 | { 63 | headers: { 64 | 'x-hello': 'world' 65 | }, 66 | status: 200 // anything over 500, CF will return the standard HTML error message. 67 | } 68 | ) 69 | }) 70 | 71 | router.get('/html', async (req) => { 72 | // An example HTML response 73 | return response.html('Hello, world!

This is a test of the easy-utils HTML response type.

') 74 | }) 75 | 76 | router.get('/cookies', async (req) => { 77 | // Example on how to use Cookies with CookieJar 78 | var cookieJar = new cookies.jar(req) 79 | 80 | var token = secrets.uuidv4() 81 | 82 | cookieJar.set('authToken', token) 83 | cookieJar.set('data', { 'hello': 'world' }) 84 | 85 | return response.json({ token, oldToken: cookieJar.get('authToken') }, { cookies: cookieJar }) 86 | }) 87 | 88 | router.get('/hash', async (req) => { 89 | // A password hash generation service example 90 | var password = new URL(req.url).searchParams.get('password') || 'heck' 91 | 92 | var pass = await secrets.hashPassword(password) 93 | 94 | var match = await secrets.validatePassword(password, pass) 95 | 96 | return response.json({ password: pass, isValid: match }) 97 | }) 98 | 99 | addEventListener('fetch', (event) => { 100 | if (event.request.method == 'OPTIONS') { 101 | return event.respondWith(response.cors()) 102 | } 103 | 104 | return event.respondWith(router.handle(event.request)) 105 | }) -------------------------------------------------------------------------------- /example.webpack.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | context: __dirname, 3 | target: "webworker", 4 | entry: "./example.js", 5 | optimization: { 6 | // so we can get better error logs in the Workers editor. 7 | minimize: false 8 | }, 9 | } -------------------------------------------------------------------------------- /example.websocket.js: -------------------------------------------------------------------------------- 1 | import { response, WebsocketResponse} from './src/index.js' 2 | 3 | addEventListener('fetch', (event) => { 4 | var server = new WebsocketResponse() 5 | 6 | server.on('message', msg => { 7 | server.send({ got: msg }) 8 | }) 9 | 10 | event.respondWith(new Promise(r => r(response.websocket(server)))) 11 | }) -------------------------------------------------------------------------------- /examples/socketio.js: -------------------------------------------------------------------------------- 1 | // Create the socket server 2 | const PORT = 8787; 3 | var socket = require('socket.io')( 4 | PORT, 5 | { 6 | cors: { 7 | origin: "*", 8 | methods: ["GET", "POST"] 9 | } 10 | }); 11 | 12 | socket.on('connection', function(client) { 13 | 14 | // Listen for test and disconnect events 15 | client.on('test', onTest); 16 | client.on('disconnect', onDisconnect); 17 | 18 | // Handle a test event from the client 19 | function onTest(data) { 20 | console.log('Received: "' + data + '" from client: ' + client.id); 21 | client.emit('test', "Cheers, " + client.id); 22 | } 23 | 24 | // Handle a disconnection from the client 25 | function onDisconnect() { 26 | console.log('Received: disconnect event from client: ' + client.id); 27 | client.removeListener('test', onTest); 28 | client.removeListener('disconnect', onDisconnect); 29 | } 30 | }); -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cfw-easy-utils", 3 | "version": "1.0.2", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@babel/code-frame": { 8 | "version": "7.12.13", 9 | "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.13.tgz", 10 | "integrity": "sha512-HV1Cm0Q3ZrpCR93tkWOYiuYIgLxZXZFVG2VgK+MBWjUqZTundupbfx2aXarXuw5Ko5aMcjtJgbSs4vUGBS5v6g==", 11 | "dev": true, 12 | "requires": { 13 | "@babel/highlight": "^7.12.13" 14 | } 15 | }, 16 | "@babel/helper-validator-identifier": { 17 | "version": "7.12.11", 18 | "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz", 19 | "integrity": "sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==", 20 | "dev": true 21 | }, 22 | "@babel/highlight": { 23 | "version": "7.12.13", 24 | "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.12.13.tgz", 25 | "integrity": "sha512-kocDQvIbgMKlWxXe9fof3TQ+gkIPOUSEYhJjqUjvKMez3krV7vbzYCDq39Oj11UAVK7JqPVGQPlgE85dPNlQww==", 26 | "dev": true, 27 | "requires": { 28 | "@babel/helper-validator-identifier": "^7.12.11", 29 | "chalk": "^2.0.0", 30 | "js-tokens": "^4.0.0" 31 | } 32 | }, 33 | "@neriko/cloudflare-workers-router": { 34 | "version": "0.1.1", 35 | "resolved": "https://registry.npmjs.org/@neriko/cloudflare-workers-router/-/cloudflare-workers-router-0.1.1.tgz", 36 | "integrity": "sha512-O9KvuiQ0wxpI9jFh88roMM6M/LSMFHKXzLhLBD1NQxe7JmhDj1VwoHujUEmkhrsP5NoOIReCPiz4wUMa+4vKTA==", 37 | "dev": true 38 | }, 39 | "@types/component-emitter": { 40 | "version": "1.2.10", 41 | "resolved": "https://registry.npmjs.org/@types/component-emitter/-/component-emitter-1.2.10.tgz", 42 | "integrity": "sha512-bsjleuRKWmGqajMerkzox19aGbscQX5rmmvvXl3wlIp5gMG1HgkiwPxsN5p070fBDKTNSPgojVbuY1+HWMbFhg==" 43 | }, 44 | "@types/cookie": { 45 | "version": "0.4.0", 46 | "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.0.tgz", 47 | "integrity": "sha512-y7mImlc/rNkvCRmg8gC3/lj87S7pTUIJ6QGjwHR9WQJcFs+ZMTOaoPrkdFA/YdbuqVEmEbb5RdhVxMkAcgOnpg==" 48 | }, 49 | "@types/cors": { 50 | "version": "2.8.10", 51 | "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.10.tgz", 52 | "integrity": "sha512-C7srjHiVG3Ey1nR6d511dtDkCEjxuN9W1HWAEjGq8kpcwmNM6JJkpC0xvabM7BXTG2wDq8Eu33iH9aQKa7IvLQ==" 53 | }, 54 | "@types/json-schema": { 55 | "version": "7.0.7", 56 | "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.7.tgz", 57 | "integrity": "sha512-cxWFQVseBm6O9Gbw1IWb8r6OS4OhSt3hPZLkFApLjM8TEXROBuQGLAH2i2gZpcXdLBIrpXuTDhH7Vbm1iXmNGA==", 58 | "dev": true 59 | }, 60 | "@types/node": { 61 | "version": "14.14.28", 62 | "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.28.tgz", 63 | "integrity": "sha512-lg55ArB+ZiHHbBBttLpzD07akz0QPrZgUODNakeC09i62dnrywr9mFErHuaPlB6I7z+sEbK+IYmplahvplCj2g==" 64 | }, 65 | "accepts": { 66 | "version": "1.3.7", 67 | "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", 68 | "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==", 69 | "requires": { 70 | "mime-types": "~2.1.24", 71 | "negotiator": "0.6.2" 72 | } 73 | }, 74 | "ajv": { 75 | "version": "6.12.6", 76 | "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", 77 | "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", 78 | "dev": true, 79 | "requires": { 80 | "fast-deep-equal": "^3.1.1", 81 | "fast-json-stable-stringify": "^2.0.0", 82 | "json-schema-traverse": "^0.4.1", 83 | "uri-js": "^4.2.2" 84 | } 85 | }, 86 | "ajv-keywords": { 87 | "version": "3.5.2", 88 | "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", 89 | "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", 90 | "dev": true 91 | }, 92 | "ansi-styles": { 93 | "version": "3.2.1", 94 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", 95 | "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", 96 | "dev": true, 97 | "requires": { 98 | "color-convert": "^1.9.0" 99 | } 100 | }, 101 | "backo2": { 102 | "version": "1.0.2", 103 | "resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz", 104 | "integrity": "sha1-MasayLEpNjRj41s+u2n038+6eUc=", 105 | "dev": true 106 | }, 107 | "base64-arraybuffer": { 108 | "version": "0.1.4", 109 | "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.4.tgz", 110 | "integrity": "sha1-mBjHngWbE1X5fgQooBfIOOkLqBI=" 111 | }, 112 | "base64id": { 113 | "version": "2.0.0", 114 | "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", 115 | "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==" 116 | }, 117 | "big.js": { 118 | "version": "5.2.2", 119 | "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", 120 | "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", 121 | "dev": true 122 | }, 123 | "buffer-from": { 124 | "version": "1.1.1", 125 | "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", 126 | "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", 127 | "dev": true 128 | }, 129 | "cfw-easy-html": { 130 | "version": "0.1.9", 131 | "resolved": "https://registry.npmjs.org/cfw-easy-html/-/cfw-easy-html-0.1.9.tgz", 132 | "integrity": "sha512-W5oxmh7bakTV1FKujQrfx2zJpzsPfl1wIAUg6wIounEXTIsP2GzxzI1vw4i/f0IldwvEykt+QXzM9QdG3sQ5ew==", 133 | "dev": true, 134 | "requires": { 135 | "raw-loader": "^4.0.2", 136 | "rollup": "^2.38.1" 137 | } 138 | }, 139 | "chalk": { 140 | "version": "2.4.2", 141 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", 142 | "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", 143 | "dev": true, 144 | "requires": { 145 | "ansi-styles": "^3.2.1", 146 | "escape-string-regexp": "^1.0.5", 147 | "supports-color": "^5.3.0" 148 | } 149 | }, 150 | "color-convert": { 151 | "version": "1.9.3", 152 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", 153 | "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", 154 | "dev": true, 155 | "requires": { 156 | "color-name": "1.1.3" 157 | } 158 | }, 159 | "color-name": { 160 | "version": "1.1.3", 161 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", 162 | "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", 163 | "dev": true 164 | }, 165 | "commander": { 166 | "version": "2.20.3", 167 | "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", 168 | "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", 169 | "dev": true 170 | }, 171 | "component-emitter": { 172 | "version": "1.3.0", 173 | "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", 174 | "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==" 175 | }, 176 | "cookie": { 177 | "version": "0.4.1", 178 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz", 179 | "integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==" 180 | }, 181 | "cors": { 182 | "version": "2.8.5", 183 | "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", 184 | "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", 185 | "requires": { 186 | "object-assign": "^4", 187 | "vary": "^1" 188 | } 189 | }, 190 | "d3-color": { 191 | "version": "2.0.0", 192 | "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-2.0.0.tgz", 193 | "integrity": "sha512-SPXi0TSKPD4g9tw0NMZFnR95XVgUZiBH+uUTqQuDu1OsE2zomHU7ho0FISciaPvosimixwHFl3WHLGabv6dDgQ==", 194 | "dev": true 195 | }, 196 | "d3-interpolate": { 197 | "version": "2.0.1", 198 | "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-2.0.1.tgz", 199 | "integrity": "sha512-c5UhwwTs/yybcmTpAVqwSFl6vrQ8JZJoT5F7xNFK9pymv5C0Ymcc9/LIJHtYIggg/yS9YHw8i8O8tgb9pupjeQ==", 200 | "dev": true, 201 | "requires": { 202 | "d3-color": "1 - 2" 203 | } 204 | }, 205 | "d3-scale-chromatic": { 206 | "version": "2.0.0", 207 | "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-2.0.0.tgz", 208 | "integrity": "sha512-LLqy7dJSL8yDy7NRmf6xSlsFZ6zYvJ4BcWFE4zBrOPnQERv9zj24ohnXKRbyi9YHnYV+HN1oEO3iFK971/gkzA==", 209 | "dev": true, 210 | "requires": { 211 | "d3-color": "1 - 2", 212 | "d3-interpolate": "1 - 2" 213 | } 214 | }, 215 | "debug": { 216 | "version": "4.3.1", 217 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", 218 | "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", 219 | "requires": { 220 | "ms": "2.1.2" 221 | } 222 | }, 223 | "emojis-list": { 224 | "version": "3.0.0", 225 | "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", 226 | "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", 227 | "dev": true 228 | }, 229 | "engine.io": { 230 | "version": "5.0.0", 231 | "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-5.0.0.tgz", 232 | "integrity": "sha512-BATIdDV3H1SrE9/u2BAotvsmjJg0t1P4+vGedImSs1lkFAtQdvk4Ev1y4LDiPF7BPWgXWEG+NDY+nLvW3UrMWw==", 233 | "requires": { 234 | "accepts": "~1.3.4", 235 | "base64id": "2.0.0", 236 | "cookie": "~0.4.1", 237 | "cors": "~2.8.5", 238 | "debug": "~4.3.1", 239 | "engine.io-parser": "~4.0.0", 240 | "ws": "~7.4.2" 241 | } 242 | }, 243 | "engine.io-client": { 244 | "version": "4.1.2", 245 | "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-4.1.2.tgz", 246 | "integrity": "sha512-1mwvwKYMa0AaCy+sPgvJ/SnKyO5MJZ1HEeXfA3Rm/KHkHGiYD5bQVq8QzvIrkI01FuVtOdZC5lWdRw1BGXB2NQ==", 247 | "dev": true, 248 | "requires": { 249 | "base64-arraybuffer": "0.1.4", 250 | "component-emitter": "~1.3.0", 251 | "debug": "~4.3.1", 252 | "engine.io-parser": "~4.0.1", 253 | "has-cors": "1.1.0", 254 | "parseqs": "0.0.6", 255 | "parseuri": "0.0.6", 256 | "ws": "~7.4.2", 257 | "xmlhttprequest-ssl": "~1.5.4", 258 | "yeast": "0.1.2" 259 | } 260 | }, 261 | "engine.io-parser": { 262 | "version": "4.0.2", 263 | "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-4.0.2.tgz", 264 | "integrity": "sha512-sHfEQv6nmtJrq6TKuIz5kyEKH/qSdK56H/A+7DnAuUPWosnIZAS2NHNcPLmyjtY3cGS/MqJdZbUjW97JU72iYg==", 265 | "requires": { 266 | "base64-arraybuffer": "0.1.4" 267 | } 268 | }, 269 | "escape-string-regexp": { 270 | "version": "1.0.5", 271 | "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", 272 | "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", 273 | "dev": true 274 | }, 275 | "fast-deep-equal": { 276 | "version": "3.1.3", 277 | "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", 278 | "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", 279 | "dev": true 280 | }, 281 | "fast-json-stable-stringify": { 282 | "version": "2.1.0", 283 | "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", 284 | "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", 285 | "dev": true 286 | }, 287 | "fsevents": { 288 | "version": "2.3.2", 289 | "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", 290 | "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", 291 | "dev": true, 292 | "optional": true 293 | }, 294 | "has-cors": { 295 | "version": "1.1.0", 296 | "resolved": "https://registry.npmjs.org/has-cors/-/has-cors-1.1.0.tgz", 297 | "integrity": "sha1-XkdHk/fqmEPRu5nCPu9J/xJv/zk=", 298 | "dev": true 299 | }, 300 | "has-flag": { 301 | "version": "3.0.0", 302 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", 303 | "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", 304 | "dev": true 305 | }, 306 | "jest-worker": { 307 | "version": "26.6.2", 308 | "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-26.6.2.tgz", 309 | "integrity": "sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ==", 310 | "dev": true, 311 | "requires": { 312 | "@types/node": "*", 313 | "merge-stream": "^2.0.0", 314 | "supports-color": "^7.0.0" 315 | }, 316 | "dependencies": { 317 | "has-flag": { 318 | "version": "4.0.0", 319 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", 320 | "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", 321 | "dev": true 322 | }, 323 | "supports-color": { 324 | "version": "7.2.0", 325 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", 326 | "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", 327 | "dev": true, 328 | "requires": { 329 | "has-flag": "^4.0.0" 330 | } 331 | } 332 | } 333 | }, 334 | "js-tokens": { 335 | "version": "4.0.0", 336 | "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", 337 | "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", 338 | "dev": true 339 | }, 340 | "json-schema-traverse": { 341 | "version": "0.4.1", 342 | "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", 343 | "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", 344 | "dev": true 345 | }, 346 | "json5": { 347 | "version": "2.2.0", 348 | "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.0.tgz", 349 | "integrity": "sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA==", 350 | "dev": true, 351 | "requires": { 352 | "minimist": "^1.2.5" 353 | } 354 | }, 355 | "loader-utils": { 356 | "version": "2.0.0", 357 | "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz", 358 | "integrity": "sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==", 359 | "dev": true, 360 | "requires": { 361 | "big.js": "^5.2.2", 362 | "emojis-list": "^3.0.0", 363 | "json5": "^2.1.2" 364 | } 365 | }, 366 | "magic-string": { 367 | "version": "0.25.7", 368 | "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.7.tgz", 369 | "integrity": "sha512-4CrMT5DOHTDk4HYDlzmwu4FVCcIYI8gauveasrdCu2IKIFOJ3f0v/8MDGJCDL9oD2ppz/Av1b0Nj345H9M+XIA==", 370 | "dev": true, 371 | "requires": { 372 | "sourcemap-codec": "^1.4.4" 373 | } 374 | }, 375 | "merge-stream": { 376 | "version": "2.0.0", 377 | "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", 378 | "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", 379 | "dev": true 380 | }, 381 | "mime-db": { 382 | "version": "1.46.0", 383 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.46.0.tgz", 384 | "integrity": "sha512-svXaP8UQRZ5K7or+ZmfNhg2xX3yKDMUzqadsSqi4NCH/KomcH75MAMYAGVlvXn4+b/xOPhS3I2uHKRUzvjY7BQ==" 385 | }, 386 | "mime-types": { 387 | "version": "2.1.29", 388 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.29.tgz", 389 | "integrity": "sha512-Y/jMt/S5sR9OaqteJtslsFZKWOIIqMACsJSiHghlCAyhf7jfVYjKBmLiX8OgpWeW+fjJ2b+Az69aPFPkUOY6xQ==", 390 | "requires": { 391 | "mime-db": "1.46.0" 392 | } 393 | }, 394 | "minimist": { 395 | "version": "1.2.5", 396 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", 397 | "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", 398 | "dev": true 399 | }, 400 | "ms": { 401 | "version": "2.1.2", 402 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", 403 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" 404 | }, 405 | "nanoevents": { 406 | "version": "5.1.11", 407 | "resolved": "https://registry.npmjs.org/nanoevents/-/nanoevents-5.1.11.tgz", 408 | "integrity": "sha512-2miSPjLe0ffTp4UzdC2nn9G7O9Pi6xBGlo86UTfJ3qYF7whtNYjY/HuRbXx+0Vk/l1lIvhBCEhd3TmfB1YAgEQ==" 409 | }, 410 | "negotiator": { 411 | "version": "0.6.2", 412 | "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", 413 | "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==" 414 | }, 415 | "object-assign": { 416 | "version": "4.1.1", 417 | "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", 418 | "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" 419 | }, 420 | "parseqs": { 421 | "version": "0.0.6", 422 | "resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.6.tgz", 423 | "integrity": "sha512-jeAGzMDbfSHHA091hr0r31eYfTig+29g3GKKE/PPbEQ65X0lmMwlEoqmhzu0iztID5uJpZsFlUPDP8ThPL7M8w==", 424 | "dev": true 425 | }, 426 | "parseuri": { 427 | "version": "0.0.6", 428 | "resolved": "https://registry.npmjs.org/parseuri/-/parseuri-0.0.6.tgz", 429 | "integrity": "sha512-AUjen8sAkGgao7UyCX6Ahv0gIK2fABKmYjvP4xmy5JaKvcbTRueIqIPHLAfq30xJddqSE033IOMUSOMCcK3Sow==", 430 | "dev": true 431 | }, 432 | "punycode": { 433 | "version": "2.1.1", 434 | "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", 435 | "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", 436 | "dev": true 437 | }, 438 | "randombytes": { 439 | "version": "2.1.0", 440 | "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", 441 | "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", 442 | "dev": true, 443 | "requires": { 444 | "safe-buffer": "^5.1.0" 445 | } 446 | }, 447 | "raw-loader": { 448 | "version": "4.0.2", 449 | "resolved": "https://registry.npmjs.org/raw-loader/-/raw-loader-4.0.2.tgz", 450 | "integrity": "sha512-ZnScIV3ag9A4wPX/ZayxL/jZH+euYb6FcUinPcgiQW0+UBtEv0O6Q3lGd3cqJ+GHH+rksEv3Pj99oxJ3u3VIKA==", 451 | "dev": true, 452 | "requires": { 453 | "loader-utils": "^2.0.0", 454 | "schema-utils": "^3.0.0" 455 | } 456 | }, 457 | "rollup": { 458 | "version": "2.38.5", 459 | "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.38.5.tgz", 460 | "integrity": "sha512-VoWt8DysFGDVRGWuHTqZzT02J0ASgjVq/hPs9QcBOGMd7B+jfTr/iqMVEyOi901rE3xq+Deq66GzIT1yt7sGwQ==", 461 | "dev": true, 462 | "requires": { 463 | "fsevents": "~2.3.1" 464 | } 465 | }, 466 | "rollup-plugin-inject-process-env": { 467 | "version": "1.3.1", 468 | "resolved": "https://registry.npmjs.org/rollup-plugin-inject-process-env/-/rollup-plugin-inject-process-env-1.3.1.tgz", 469 | "integrity": "sha512-kKDoL30IZr0wxbNVJjq+OS92RJSKRbKV6B5eNW4q3mZTFqoWDh6lHy+mPDYuuGuERFNKXkG+AKxvYqC9+DRpKQ==", 470 | "dev": true, 471 | "requires": { 472 | "magic-string": "^0.25.7" 473 | } 474 | }, 475 | "rollup-plugin-terser": { 476 | "version": "7.0.2", 477 | "resolved": "https://registry.npmjs.org/rollup-plugin-terser/-/rollup-plugin-terser-7.0.2.tgz", 478 | "integrity": "sha512-w3iIaU4OxcF52UUXiZNsNeuXIMDvFrr+ZXK6bFZ0Q60qyVfq4uLptoS4bbq3paG3x216eQllFZX7zt6TIImguQ==", 479 | "dev": true, 480 | "requires": { 481 | "@babel/code-frame": "^7.10.4", 482 | "jest-worker": "^26.2.1", 483 | "serialize-javascript": "^4.0.0", 484 | "terser": "^5.0.0" 485 | } 486 | }, 487 | "safe-buffer": { 488 | "version": "5.2.1", 489 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", 490 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", 491 | "dev": true 492 | }, 493 | "schema-utils": { 494 | "version": "3.0.0", 495 | "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.0.0.tgz", 496 | "integrity": "sha512-6D82/xSzO094ajanoOSbe4YvXWMfn2A//8Y1+MUqFAJul5Bs+yn36xbK9OtNDcRVSBJ9jjeoXftM6CfztsjOAA==", 497 | "dev": true, 498 | "requires": { 499 | "@types/json-schema": "^7.0.6", 500 | "ajv": "^6.12.5", 501 | "ajv-keywords": "^3.5.2" 502 | } 503 | }, 504 | "serialize-javascript": { 505 | "version": "4.0.0", 506 | "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-4.0.0.tgz", 507 | "integrity": "sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==", 508 | "dev": true, 509 | "requires": { 510 | "randombytes": "^2.1.0" 511 | } 512 | }, 513 | "socket.io": { 514 | "version": "4.0.0", 515 | "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.0.0.tgz", 516 | "integrity": "sha512-/c1riZMV/4yz7KEpaMhDQbwhJDIoO55whXaRKgyEBQrLU9zUHXo9rzeTMvTOqwL9mbKfHKdrXcMoCeQ/1YtMsg==", 517 | "requires": { 518 | "@types/cookie": "^0.4.0", 519 | "@types/cors": "^2.8.8", 520 | "@types/node": ">=10.0.0", 521 | "accepts": "~1.3.4", 522 | "base64id": "~2.0.0", 523 | "debug": "~4.3.1", 524 | "engine.io": "~5.0.0", 525 | "socket.io-adapter": "~2.2.0", 526 | "socket.io-parser": "~4.0.3" 527 | } 528 | }, 529 | "socket.io-adapter": { 530 | "version": "2.2.0", 531 | "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.2.0.tgz", 532 | "integrity": "sha512-rG49L+FwaVEwuAdeBRq49M97YI3ElVabJPzvHT9S6a2CWhDKnjSFasvwAwSYPRhQzfn4NtDIbCaGYgOCOU/rlg==" 533 | }, 534 | "socket.io-client": { 535 | "version": "3.1.2", 536 | "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-3.1.2.tgz", 537 | "integrity": "sha512-fXhF8plHrd7U14A7K0JPOmZzpmGkLpIS6623DzrBZqYzI/yvlP4fA3LnxwthEVgiHmn2uJ4KjdnQD8A03PuBWQ==", 538 | "dev": true, 539 | "requires": { 540 | "@types/component-emitter": "^1.2.10", 541 | "backo2": "~1.0.2", 542 | "component-emitter": "~1.3.0", 543 | "debug": "~4.3.1", 544 | "engine.io-client": "~4.1.0", 545 | "parseuri": "0.0.6", 546 | "socket.io-parser": "~4.0.4" 547 | } 548 | }, 549 | "socket.io-parser": { 550 | "version": "4.0.4", 551 | "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.0.4.tgz", 552 | "integrity": "sha512-t+b0SS+IxG7Rxzda2EVvyBZbvFPBCjJoyHuE0P//7OAsN23GItzDRdWa6ALxZI/8R5ygK7jAR6t028/z+7295g==", 553 | "requires": { 554 | "@types/component-emitter": "^1.2.10", 555 | "component-emitter": "~1.3.0", 556 | "debug": "~4.3.1" 557 | } 558 | }, 559 | "source-map": { 560 | "version": "0.7.3", 561 | "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", 562 | "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", 563 | "dev": true 564 | }, 565 | "source-map-support": { 566 | "version": "0.5.19", 567 | "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz", 568 | "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==", 569 | "dev": true, 570 | "requires": { 571 | "buffer-from": "^1.0.0", 572 | "source-map": "^0.6.0" 573 | }, 574 | "dependencies": { 575 | "source-map": { 576 | "version": "0.6.1", 577 | "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", 578 | "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", 579 | "dev": true 580 | } 581 | } 582 | }, 583 | "sourcemap-codec": { 584 | "version": "1.4.8", 585 | "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", 586 | "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", 587 | "dev": true 588 | }, 589 | "supports-color": { 590 | "version": "5.5.0", 591 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", 592 | "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", 593 | "dev": true, 594 | "requires": { 595 | "has-flag": "^3.0.0" 596 | } 597 | }, 598 | "terser": { 599 | "version": "5.6.0", 600 | "resolved": "https://registry.npmjs.org/terser/-/terser-5.6.0.tgz", 601 | "integrity": "sha512-vyqLMoqadC1uR0vywqOZzriDYzgEkNJFK4q9GeyOBHIbiECHiWLKcWfbQWAUaPfxkjDhapSlZB9f7fkMrvkVjA==", 602 | "dev": true, 603 | "requires": { 604 | "commander": "^2.20.0", 605 | "source-map": "~0.7.2", 606 | "source-map-support": "~0.5.19" 607 | } 608 | }, 609 | "uri-js": { 610 | "version": "4.4.1", 611 | "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", 612 | "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", 613 | "dev": true, 614 | "requires": { 615 | "punycode": "^2.1.0" 616 | } 617 | }, 618 | "vary": { 619 | "version": "1.1.2", 620 | "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", 621 | "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" 622 | }, 623 | "ws": { 624 | "version": "7.4.4", 625 | "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.4.tgz", 626 | "integrity": "sha512-Qm8k8ojNQIMx7S+Zp8u/uHOx7Qazv3Yv4q68MiWWWOJhiwG5W3x7iqmRtJo8xxrciZUY4vRxUTJCKuRnF28ZZw==" 627 | }, 628 | "xmlhttprequest-ssl": { 629 | "version": "1.5.5", 630 | "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.5.tgz", 631 | "integrity": "sha1-wodrBhaKrcQOV9l+gRkayPQ5iz4=", 632 | "dev": true 633 | }, 634 | "yeast": { 635 | "version": "0.1.2", 636 | "resolved": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz", 637 | "integrity": "sha1-AI4G2AlDIMNy28L47XagymyKxBk=", 638 | "dev": true 639 | } 640 | } 641 | } 642 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cfw-easy-utils", 3 | "version": "1.0.3", 4 | "sideEffects": false, 5 | "description": "An utils library for common tasks with CloudFlare Workers.", 6 | "main": "dist/cfw-easy-utils.js", 7 | "scripts": { 8 | "inc": "npm version patch --no-git-tag-version && git add package.json && git commit -m \"Auto-increment version.\" && git push", 9 | "build": "rollup -c && node postBuild.js", 10 | "test": "echo \"Error: no test specified\" && exit 1" 11 | }, 12 | "keywords": [ 13 | "workers", 14 | "cloudflare", 15 | "cfw", 16 | "utils" 17 | ], 18 | "author": "Cerulean", 19 | "license": "MIT", 20 | "devDependencies": { 21 | "@neriko/cloudflare-workers-router": "^0.1.1", 22 | "rollup": "^2.38.5", 23 | "rollup-plugin-inject-process-env": "^1.3.1", 24 | "cfw-easy-html": "^0.1.9", 25 | "rollup-plugin-terser": "^7.0.2", 26 | "d3-scale-chromatic": "^2.0.0", 27 | "socket.io-client": "^3.1.2" 28 | }, 29 | "dependencies": { 30 | "cookie": "^0.4.1", 31 | "nanoevents": "^5.1.11", 32 | "socket.io": "^4.0.0" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /postBuild.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs') 2 | fs.readFile('./package.json', 'utf8', function (err,data) { 3 | if (err) { 4 | return console.log(err); 5 | } 6 | var packageVersion = JSON.parse(data).version 7 | fs.readFile('./dist/cfw-easy-utils.js', 'utf8', function (err,data) { 8 | var result = data.replace('{{ packageVersion }}', packageVersion) 9 | 10 | fs.writeFile('./dist/cfw-easy-utils.js', result, 'utf8', function (err) { 11 | if (err) return console.log(err); 12 | }); 13 | }) 14 | 15 | }); -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | 2 | import injectProcessEnv from 'rollup-plugin-inject-process-env'; 3 | import { terser } from "rollup-plugin-terser"; 4 | 5 | export default [{ 6 | input: 'src/index.js', 7 | output: [{ 8 | file: 'dist/cfw-easy-utils.js', 9 | format: 'cjs', 10 | plugins: [terser()] 11 | }] 12 | }] -------------------------------------------------------------------------------- /src/cookies.js: -------------------------------------------------------------------------------- 1 | var cookie = require('cookie'); 2 | 3 | class Jar { 4 | constructor (request) { 5 | this.request = request 6 | 7 | if (request) { 8 | this.cookies = cookie.parse(request.headers.get('cookie') || '') 9 | } else { 10 | this.cookies = {} 11 | } 12 | 13 | this.futureCookies = {} // if we ever want to set cookies in the future, this is the source object 14 | } 15 | 16 | get(name) { 17 | return this.cookies[name] 18 | } 19 | 20 | set(name, value, options) { 21 | this.futureCookies[name] = { 22 | value, 23 | ...options 24 | } 25 | } 26 | 27 | __remove() { 28 | // entirely remove a cookie 29 | // TODO at somepoint if requested 30 | } 31 | 32 | values() { 33 | // returns an array of valid header values. 34 | var output = [] 35 | for (const [key, options] of Object.entries(this.futureCookies)) { 36 | output.push(cookie.serialize(key, options.value, options)) 37 | } 38 | 39 | return output 40 | } 41 | } 42 | 43 | export const cookies = { 44 | jar: Jar 45 | } -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | export * from './cookies.js' 2 | export * from './secrets.js' 3 | export * from './stopwatch.js' 4 | export * from './websockets.js' 5 | 6 | var version = '{{ packageVersion }}' 7 | 8 | export const response = { 9 | version, 10 | 11 | config: { 12 | // For future HMAC generation 13 | secretKey: 'password', 14 | debugHeaders: false 15 | }, 16 | 17 | accessControl: { 18 | allowOrigin: '*', 19 | allowMethods: 'GET, POST, PUT', 20 | allowHeaders: 'Content-Type', 21 | }, 22 | 23 | request: null, // if this is set, we need to use the origin of the request for our CORS headers. 24 | 25 | _corsHeaders() { 26 | var origin = this.accessControl.allowOrigin 27 | 28 | if (this.request) { 29 | // we have a request object, lets use its origin as our CORS origin. 30 | origin = new URL(this.request.url).origin 31 | } 32 | 33 | return { 34 | 'Access-Control-Allow-Origin': origin, 35 | 'Access-Control-Allow-Methods': this.accessControl.allowMethods, 36 | 'Access-Control-Max-Age': '1728000' 37 | } 38 | }, 39 | 40 | injectCors(response, options) { 41 | // modify a response object to have CORS headers. 42 | var headers = this._corsHeaders() 43 | }, 44 | 45 | _genericResponse(mimetype, body, options) { 46 | // helper function to make developing this easier. 47 | if (typeof options === 'undefined') { var options = {} } 48 | var extraHeaders = options.headers || {} 49 | var status = options.status || 200 50 | var statusText = options.statusText || 'OK' 51 | var autoCors = options.autoCors 52 | // use different method since we want a bool which can be false. 53 | if (typeof options.autoCors === 'undefined') { autoCors = true } 54 | 55 | var cookies = options.cookies || null 56 | var stopwatch = options.stopwatch || null 57 | 58 | var headers = { 59 | 'Content-Type': mimetype, 60 | ...extraHeaders 61 | } 62 | 63 | if (autoCors) { 64 | headers = { 65 | ...headers, 66 | ...this._corsHeaders() 67 | } 68 | } 69 | 70 | var resp = new Response( 71 | body, 72 | { 73 | status, 74 | statusText, 75 | headers 76 | } 77 | ) 78 | 79 | if (cookies) { 80 | var val = cookies.values() 81 | 82 | val.forEach(header => { 83 | resp.headers.append('Set-Cookie', header) 84 | }) 85 | } 86 | 87 | if (stopwatch) { 88 | resp.headers.set('Server-Timing', stopwatch.getHeader()) 89 | } 90 | 91 | if (this.config.debugHeaders) { 92 | resp.headers.set('x-cfw-eu-version', this.version) 93 | } 94 | 95 | 96 | return new Promise(res => res(resp)) 97 | }, 98 | 99 | cors(request) { 100 | if (request) { 101 | // set request so we can make our headers origin-aware. 102 | this.request = request 103 | } 104 | 105 | return new Response(null, { headers: this._corsHeaders() }) 106 | }, 107 | 108 | json(stringOrObject, options) { 109 | // turns a JSON body into a response object with valid headers. 110 | var body = stringOrObject 111 | 112 | if (typeof body != 'string') { 113 | // presume that this is an not already encoded JSON 114 | // string so we need to force it to JSON. 115 | body = JSON.stringify(stringOrObject) 116 | } 117 | 118 | return this._genericResponse( 119 | 'application/json', 120 | body, 121 | options 122 | ) 123 | }, 124 | 125 | html(body, options) { 126 | return this._genericResponse( 127 | 'text/html', 128 | body, 129 | options 130 | ) 131 | }, 132 | 133 | text(body, options) { 134 | return this._genericResponse( 135 | 'plain/text', 136 | body, 137 | options 138 | ) 139 | }, 140 | 141 | fromResponse(resp, options) { 142 | var oldHeaders = this.headersToObject(resp.headers) 143 | 144 | var headers = { 145 | ...oldHeaders, 146 | ...options.headers || {} 147 | } 148 | 149 | if ('headers' in options) { 150 | delete options.headers 151 | } 152 | 153 | var contentType = JSON.parse(JSON.stringify(headers['content-type'])) // force new String 154 | 155 | if ('content-type' in headers) { 156 | delete headers['content-type'] // to stop multiple Content-Type headers. 157 | } 158 | 159 | return this._genericResponse( 160 | contentType, 161 | resp.body, 162 | { 163 | headers, 164 | ...options 165 | } 166 | ) 167 | }, 168 | 169 | async static(request, options) { 170 | var baseUrl = options.baseUrl 171 | if (!baseUrl) { 172 | throw 'You need to specify a baseUrl for response.static to work.' 173 | } 174 | 175 | let url = new URL(request.url) 176 | 177 | // wrap this fetch in our custom Response formatter 178 | var resp = await fetch( 179 | `${baseUrl}${url.pathname.replace(options.routePrefix || '<>', '')}`, 180 | { 181 | cf: { 182 | cacheTtl: options.ttl || 600, 183 | } 184 | } 185 | ) 186 | 187 | return this.fromResponse(resp, options) 188 | }, 189 | 190 | async websocket(socket) { 191 | return new Response(null, { status: 101, webSocket: socket.client }) 192 | }, 193 | 194 | setHeader(response, key, value) { 195 | var resp = new Response(response.body, response) 196 | var val = value 197 | if (typeof value.values == 'function') { 198 | // our values function returns an array 199 | val = value.values() 200 | 201 | val.forEach(header => { 202 | resp.headers.append(key, header) 203 | }) 204 | } else { 205 | resp.headers.append(key, val) 206 | } 207 | 208 | return resp 209 | }, 210 | 211 | headersToObject(headers) { 212 | var object = Object.fromEntries(headers.entries()) 213 | return object 214 | } 215 | } -------------------------------------------------------------------------------- /src/secrets.js: -------------------------------------------------------------------------------- 1 | // I wanted to call this "crypto", but that would overwrite the real crypto library in JS :( 2 | 3 | function bufferToHexString(buffer) { 4 | var s = '', h = '0123456789abcdef'; 5 | (new Uint8Array(buffer)).forEach((v) => { s += h[v >> 4] + h[v & 15]; }); 6 | return s; 7 | } 8 | 9 | function hexStringToArrayBuffer(hexString) { 10 | // remove the leading 0x 11 | hexString = hexString.replace(/^0x/, ''); 12 | 13 | // ensure even number of characters 14 | if (hexString.length % 2 != 0) { 15 | console.log('WARNING: expecting an even number of characters in the hexString'); 16 | } 17 | 18 | // check for some non-hex characters 19 | var bad = hexString.match(/[G-Z\s]/i); 20 | if (bad) { 21 | console.log('WARNING: found non-hex characters', bad); 22 | } 23 | 24 | // split the string into pairs of octets 25 | var pairs = hexString.match(/[\dA-F]{2}/gi); 26 | 27 | // convert the octets to integers 28 | var integers = pairs.map(function(s) { 29 | return parseInt(s, 16); 30 | }); 31 | 32 | var array = new Uint8Array(integers); 33 | 34 | return array.buffer; 35 | } 36 | 37 | export const secrets = { 38 | uuidv4() { 39 | return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, c => 40 | (c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16) 41 | ) 42 | }, 43 | async hashPassword(password, options) { 44 | var options = options || {} 45 | var salt = options.salt || crypto.getRandomValues(new Uint8Array(8)) 46 | var iterations = options.iterations || 45000 47 | 48 | if (typeof salt == 'string') { 49 | // this means we are validating a password 50 | // or the user is using a String salt 51 | salt = hexStringToArrayBuffer(salt) 52 | } 53 | 54 | const encoder = new TextEncoder('utf-8') 55 | 56 | const passphraseKey = encoder.encode(password) 57 | 58 | const key = await crypto.subtle.importKey( 59 | 'raw', 60 | passphraseKey, 61 | {name: 'PBKDF2'}, 62 | false, 63 | ['deriveBits', 'deriveKey'] 64 | ) 65 | 66 | const webKey = await crypto.subtle.deriveKey( 67 | { 68 | name: 'PBKDF2', 69 | salt, 70 | iterations, 71 | hash: 'SHA-256' 72 | }, 73 | key, 74 | // Don't actually need a cipher suite, 75 | // but api requires it is specified. 76 | { name: 'AES-CBC', length: 256 }, 77 | true, 78 | [ "encrypt", "decrypt" ] 79 | ) 80 | 81 | const hash = await crypto.subtle.exportKey("raw", webKey) 82 | 83 | // return a easy-utils password hash that can be stored in KV 84 | // contains all of the info we need to check later. 85 | return `$PBKDF2;h=${bufferToHexString(hash)};s=${bufferToHexString(salt)};i=${iterations};` 86 | }, 87 | async validatePassword(password, existingPassword) { 88 | var options = { 89 | salt: existingPassword.split(';s=')[1].split(';')[0], 90 | iterations: parseInt(existingPassword.split(';i=')[1].split(';')[0]) 91 | } 92 | 93 | var hashedPassword = await this.hashPassword(password, options) 94 | 95 | var isValid = true 96 | 97 | // we don't return right away to stop key based timing attacks 98 | for (var i = 0; i < hashedPassword.length; i++) { 99 | if (hashedPassword.charAt(i) != existingPassword.charAt(i)) { 100 | isValid = false // this can only be set to false. 101 | } 102 | } 103 | 104 | return isValid 105 | } 106 | } -------------------------------------------------------------------------------- /src/stopwatch.js: -------------------------------------------------------------------------------- 1 | export class Stopwatch { 2 | constructor () { 3 | this.start = new Date() 4 | this.checkpoints = [ 5 | { 6 | name: 'Start', 7 | dur: 0, 8 | desc: 'Started recording' 9 | } 10 | ] 11 | this.lastCheckpointTime = new Date() 12 | } 13 | 14 | mark(name, options) { 15 | this.checkpoints.push({ 16 | name, 17 | time: new Date(), 18 | dur: new Date() - this.lastCheckpointTime, 19 | ...options 20 | }) 21 | 22 | this.lastCheckpointTime = new Date() 23 | } 24 | 25 | getTotalTime() { 26 | // returns total time in MS 27 | return new Date() - this.start 28 | } 29 | 30 | join(stopwatchInstance) { 31 | // combine two different Stopwatch instances 32 | var combined = this.checkpoints + stopwatchInstance.checkpoints 33 | this.checkpoints = combined.sort((a, b) => b.time - a.time) 34 | } 35 | 36 | getHeader() { 37 | // returns a Server-Timing header 38 | var out = [] 39 | this.checkpoints.forEach((point) => { 40 | var values = [point.name] 41 | Object.keys(point).forEach(key => { 42 | if (key == 'name' || key == 'time') {return} // ignore the name value 43 | values.push(`${key}=${point[key]}`) 44 | }) 45 | out.push(values.join(';')) 46 | }) 47 | 48 | return out.join(',') 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/websockets.js: -------------------------------------------------------------------------------- 1 | import { createNanoEvents } from "nanoevents" 2 | 3 | export class Websocket { 4 | constructor (url, options) { 5 | this.url = url 6 | this.options = options 7 | this.emitter = createNanoEvents() 8 | this.sendQueue = [] 9 | 10 | if (this.url.includes('wss:')) { 11 | this.url = this.url.replace('wss:', 'https:') 12 | } 13 | 14 | if (this.url.includes('ws:')) { 15 | this.url = this.url.replace('ws:', 'http:') 16 | } 17 | 18 | this.connect() 19 | } 20 | 21 | log(msg) { 22 | if (this.options.logger) {this.options.logger.send(msg)} 23 | } 24 | 25 | async connect() { 26 | // Undocumented API at time of writing. 27 | var resp = await fetch(this.url, { headers: {'upgrade': 'websocket'} }) 28 | this.socket = resp.webSocket 29 | this.socket.accept() 30 | 31 | this.socket.addEventListener('message', (msg) => { 32 | this.emitter.emit('rawmessage', msg) 33 | 34 | var data = msg.data 35 | try { 36 | data = JSON.parse(data) 37 | if (typeof data == 'string') { 38 | // JSON convert returned a String, that means the original was a string. 39 | // we want to return the original frame as we dont want to add extra quotes to the string. 40 | data = msg.data 41 | } 42 | } catch (e) { 43 | // ignore parser errors 44 | } 45 | 46 | this.emitter.emit('message', data) 47 | }) 48 | 49 | this.socket.addEventListener('close', () => { 50 | this.emitter.emit('close') 51 | }) 52 | 53 | this.sendQueue.forEach(queued => { 54 | this.socket.send(queued) 55 | }) 56 | 57 | this.sendQueue = [] 58 | } 59 | 60 | on(event, callback) { 61 | return this.emitter.on(event, callback) 62 | } 63 | 64 | addEventListener(event, callback) { 65 | if (event == 'message') { 66 | event = 'rawmessage' 67 | } 68 | return this.emitter.on(event, callback) 69 | } 70 | 71 | send(data, options) { 72 | var toSend = data 73 | 74 | if (data.constructor == Object || data.constructor == Array) { 75 | toSend = JSON.stringify(toSend) 76 | } 77 | 78 | if (!this.socket) { 79 | this.sendQueue.push(toSend) 80 | } else { 81 | this.socket.send(toSend) 82 | } 83 | } 84 | } 85 | 86 | export class WebsocketResponse { 87 | constructor () { 88 | let pair = new WebSocketPair() 89 | this.socket = pair[1] 90 | this.client = pair[0] 91 | 92 | // accept our end of the socket 93 | this.socket.accept() 94 | 95 | this.emitter = createNanoEvents() 96 | 97 | this.session = { 98 | history: [], 99 | startTime: new Date(), 100 | lastMessageTime: null, 101 | } 102 | 103 | this.socket.addEventListener('message', (msg) => { 104 | var data = msg.data 105 | 106 | try { 107 | data = JSON.stringify(data) 108 | 109 | if (typeof data == 'string') { 110 | // dont JSON wrap data that is already a string. 111 | data = msg.data 112 | } 113 | } catch (e) { 114 | // couldn't parse incoming data 115 | } 116 | 117 | this.session.lastMessageTime = new Date() 118 | 119 | this.emitter.emit('message', data) 120 | }) 121 | 122 | this.socket.addEventListener('close', () => this.emitter.emit('close')) 123 | } 124 | 125 | on(event, callback) { 126 | return this.emitter.on(event, callback) 127 | } 128 | 129 | send(data, options) { 130 | var toSend = data 131 | 132 | if (data.constructor == Object || data.constructor == Array) { 133 | toSend = JSON.stringify(toSend) 134 | } 135 | 136 | this.socket.send(toSend) 137 | } 138 | } -------------------------------------------------------------------------------- /wrangler.toml: -------------------------------------------------------------------------------- 1 | name = "cfw-easy-utils" 2 | type = "webpack" 3 | account_id = "2dcc5a4f486a50625b4167e90d113158" 4 | workers_dev = true 5 | route = "" 6 | zone_id = "" 7 | 8 | webpack_config = "example.webpack.config.js" --------------------------------------------------------------------------------