├── .editorconfig ├── .github └── workflows │ └── deploy.yml ├── .gitignore ├── .gitpod.yml ├── LICENSE ├── README.md ├── package.json ├── pnpm-lock.yaml ├── src └── index.ts ├── tsconfig.json └── wrangler.toml /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | workflow_dispatch: 8 | 9 | jobs: 10 | deploy: 11 | runs-on: ubuntu-latest 12 | name: Deploy 13 | steps: 14 | - uses: actions/checkout@v3 15 | - uses: pnpm/action-setup@v2.2.4 16 | - uses: actions/setup-node@v3 17 | with: 18 | node-version: '16' 19 | cache: 'pnpm' 20 | - name: Install dependencies 21 | run: pnpm install --frozen-lockfile 22 | - name: Publish 23 | run: pnpm run deploy 24 | env: 25 | CLOUDFLARE_API_TOKEN: ${{ secrets.CF_API_TOKEN }} 26 | CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CF_ACCOUNT_ID }} 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | 3 | logs 4 | _.log 5 | npm-debug.log_ 6 | yarn-debug.log* 7 | yarn-error.log* 8 | lerna-debug.log* 9 | .pnpm-debug.log* 10 | 11 | # Diagnostic reports (https://nodejs.org/api/report.html) 12 | 13 | report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json 14 | 15 | # Runtime data 16 | 17 | pids 18 | _.pid 19 | _.seed 20 | \*.pid.lock 21 | 22 | # Directory for instrumented libs generated by jscoverage/JSCover 23 | 24 | lib-cov 25 | 26 | # Coverage directory used by tools like istanbul 27 | 28 | coverage 29 | \*.lcov 30 | 31 | # nyc test coverage 32 | 33 | .nyc_output 34 | 35 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 36 | 37 | .grunt 38 | 39 | # Bower dependency directory (https://bower.io/) 40 | 41 | bower_components 42 | 43 | # node-waf configuration 44 | 45 | .lock-wscript 46 | 47 | # Compiled binary addons (https://nodejs.org/api/addons.html) 48 | 49 | build/Release 50 | 51 | # Dependency directories 52 | 53 | node_modules/ 54 | jspm_packages/ 55 | 56 | # Snowpack dependency directory (https://snowpack.dev/) 57 | 58 | web_modules/ 59 | 60 | # TypeScript cache 61 | 62 | \*.tsbuildinfo 63 | 64 | # Optional npm cache directory 65 | 66 | .npm 67 | 68 | # Optional eslint cache 69 | 70 | .eslintcache 71 | 72 | # Optional stylelint cache 73 | 74 | .stylelintcache 75 | 76 | # Microbundle cache 77 | 78 | .rpt2_cache/ 79 | .rts2_cache_cjs/ 80 | .rts2_cache_es/ 81 | .rts2_cache_umd/ 82 | 83 | # Optional REPL history 84 | 85 | .node_repl_history 86 | 87 | # Output of 'npm pack' 88 | 89 | \*.tgz 90 | 91 | # Yarn Integrity file 92 | 93 | .yarn-integrity 94 | 95 | # dotenv environment variable files 96 | 97 | .env 98 | .env.development.local 99 | .env.test.local 100 | .env.production.local 101 | .env.local 102 | 103 | # parcel-bundler cache (https://parceljs.org/) 104 | 105 | .cache 106 | .parcel-cache 107 | 108 | # Next.js build output 109 | 110 | .next 111 | out 112 | 113 | # Nuxt.js build / generate output 114 | 115 | .nuxt 116 | dist 117 | 118 | # Gatsby files 119 | 120 | .cache/ 121 | 122 | # Comment in the public line in if your project uses Gatsby and not Next.js 123 | 124 | # https://nextjs.org/blog/next-9-1#public-directory-support 125 | 126 | # public 127 | 128 | # vuepress build output 129 | 130 | .vuepress/dist 131 | 132 | # vuepress v2.x temp and cache directory 133 | 134 | .temp 135 | .cache 136 | 137 | # Docusaurus cache and generated files 138 | 139 | .docusaurus 140 | 141 | # Serverless directories 142 | 143 | .serverless/ 144 | 145 | # FuseBox cache 146 | 147 | .fusebox/ 148 | 149 | # DynamoDB Local files 150 | 151 | .dynamodb/ 152 | 153 | # TernJS port file 154 | 155 | .tern-port 156 | 157 | # Stores VSCode versions used for testing VSCode extensions 158 | 159 | .vscode-test 160 | 161 | # yarn v2 162 | 163 | .yarn/cache 164 | .yarn/unplugged 165 | .yarn/build-state.yml 166 | .yarn/install-state.gz 167 | .pnp.\* 168 | 169 | # wrangler project 170 | 171 | .dev.vars 172 | -------------------------------------------------------------------------------- /.gitpod.yml: -------------------------------------------------------------------------------- 1 | tasks: 2 | - init: pnpm install 3 | command: pnpm run dev 4 | ports: 5 | - port: 8976 6 | name: Wrangler Authorization 7 | onOpen: ignore 8 | - port: 8787 9 | name: Wrangler Dev 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Kot 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Render 2 | 3 | Proxies readonly requests to [Cloudflare R2](https://developers.cloudflare.com/r2) via [Cloudflare Workers](https://workers.dev). 4 | 5 | If you see a bug or something missing, please open an issue or pull request! 6 | 7 | ## Features 8 | - File listings (with optional hidden files)! 9 | 10 | ![screenshot of file listings in light mode](https://user-images.githubusercontent.com/33439542/193165135-1dd935f5-b68b-495a-97cc-9c69c3c0ce01.png) 11 | ![screenshot of file listings in dark mode](https://user-images.githubusercontent.com/33439542/193165189-3cd4b79e-27ea-4397-bb80-f3ccf31185dc.png) 12 | 13 | 14 | - Handles `HEAD`, `GET`, and `OPTIONS` requests 15 | - Forwards caching headers (`etag`, `cache-control`, `expires`, `last-modified`) 16 | - Forwards content headers (`content-type`, `content-encoding`, `content-language`, `content-disposition`) 17 | - Caches served files using the [Cache API](https://developers.cloudflare.com/workers/runtime-apis/cache/) 18 | - Ranged requests (`range`, `if-range`, returns `content-range`) 19 | - Handles precondition headers (`if-modified-since`, `if-unmodified-since`, `if-match`, `if-none-match`) 20 | - Can serve an appended path if the requested url ends with / - Defaults to `index.html` in 0.5.0 21 | - Can serve custom 404 responses if a file is not found 22 | 23 | ## Setup 24 | 25 | ### Installing wrangler 26 | 27 | ```sh 28 | npm i -g wrangler 29 | wrangler login 30 | ``` 31 | 32 | ### Configuration 33 | 34 | Create your R2 bucket(s) if you haven't already (replace `bucket_name` and `preview_bucket_name` appropriately): 35 | ```sh 36 | wrangler r2 bucket create bucket_name # required 37 | wrangler r2 bucket create preview_bucket_name # optional 38 | ``` 39 | You can also do this from the [Cloudflare dashboard](https://dash.cloudflare.com/?to=/:account/r2/buckets/new). 40 | 41 | Edit `wrangler.toml` to have the correct `bucket_name` and optionally, `preview_bucket_name` (you can set it to `bucket_name`) if you're going to run this locally. 42 | You can do this from a fork, if using the [GitHub Actions method](#method-2-github-actions). 43 | 44 | You may edit `CACHE_CONTROL` to the default [`cache-control` header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control) or remove it entirely to fall back to nothing. If you set `CACHE_CONTROL` to `"no-store"` then Cloudflare caching will not be used. 45 | 46 | ### Deploying 47 | 48 | Note: Due to how custom domains for workers work, you MUST use a route to take advantage of caching. Cloudflare may fix this soon. 49 | Also note that \*.workers.dev domains do not cache responses. You MUST use a route to your own (sub)domain. 50 | 51 | If you want to deploy render with multiple domains for one worker, check out [multi-render](https://github.com/Erisa/multi-render)! It uses render [as a package](#using-as-a-package) to serve multiple buckets to multiple domains with custom configurations. 52 | 53 | #### Method 1 (Local) 54 | ```sh 55 | npm install 56 | wrangler publish # or `npm run deploy` 57 | ``` 58 | 59 | #### Method 2 (GitHub Actions) 60 | 1. Fork this repository 61 | 2. Set the secrets [`CF_API_TOKEN`](https://dash.cloudflare.com/profile/api-tokens) (with `Worker Scripts: Edit` permissions) and `CF_ACCOUNT_ID` in the repo settings 62 | 3. Enable workflows in the Actions tab 63 | 4. Update `wrangler.toml` as needed (this will trigger the workflow) 64 | 5. (Optionally) set the worker route in the Cloudflare dashboard to use the Cache API 65 | 66 | ## Using as a package 67 | 68 | You may use this worker's functionality as a package by installing and importing [`render2`](https://www.npmjs.com/package/render2): 69 | ```sh 70 | npm install render2 71 | ``` 72 | Usage: 73 | ```js 74 | import render from "render2"; 75 | render.fetch(req, env, ctx); 76 | ``` 77 | 78 | You can see an awesome example with [Erisa](https://github.com/Erisa)'s [multi-render](https://github.com/Erisa/multi-render)! 79 | 80 | ## Development 81 | 82 | Install deps: 83 | ```sh 84 | npm install 85 | ``` 86 | 87 | To launch the development server: 88 | ```sh 89 | npm run dev 90 | ``` 91 | 92 | ## Notable Forks 93 | 94 | - [auravoid](https://github.com/auravoid)'s fork adds [Plausible](https://plausible.io) support. 95 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "render2", 3 | "version": "1.2.0", 4 | "author": "kotx", 5 | "description": "A Cloudflare worker for proxying readonly requests to Cloudflare R2", 6 | "license": "MIT", 7 | "main": "dist/index.js", 8 | "types": "dist/index.d.ts", 9 | "files": [ 10 | "dist" 11 | ], 12 | "scripts": { 13 | "dev": "wrangler dev", 14 | "deploy": "wrangler publish", 15 | "prepublishOnly": "tsc" 16 | }, 17 | "devDependencies": { 18 | "@cloudflare/workers-types": "^3.11.0", 19 | "@types/range-parser": "^1.2.4", 20 | "typescript": "^4.7.2", 21 | "wrangler": "^2.20.0" 22 | }, 23 | "dependencies": { 24 | "range-parser": "^1.2.1" 25 | }, 26 | "packageManager": "pnpm@8.5.1" 27 | } 28 | -------------------------------------------------------------------------------- /pnpm-lock.yaml: -------------------------------------------------------------------------------- 1 | lockfileVersion: '6.0' 2 | 3 | dependencies: 4 | range-parser: 5 | specifier: ^1.2.1 6 | version: 1.2.1 7 | 8 | devDependencies: 9 | '@cloudflare/workers-types': 10 | specifier: ^3.11.0 11 | version: 3.11.0 12 | '@types/range-parser': 13 | specifier: ^1.2.4 14 | version: 1.2.4 15 | typescript: 16 | specifier: ^4.7.2 17 | version: 4.7.2 18 | wrangler: 19 | specifier: ^2.20.0 20 | version: 2.20.0 21 | 22 | packages: 23 | 24 | /@cloudflare/kv-asset-handler@0.2.0: 25 | resolution: {integrity: sha512-MVbXLbTcAotOPUj0pAMhVtJ+3/kFkwJqc5qNOleOZTv6QkZZABDMS21dSrSlVswEHwrpWC03e4fWytjqKvuE2A==} 26 | dependencies: 27 | mime: 3.0.0 28 | dev: true 29 | 30 | /@cloudflare/workers-types@3.11.0: 31 | resolution: {integrity: sha512-XmKgZZHrCdPsoVQkdd365R7GvwTwDVJsMyEG3Dq/Tgxz6vgrps2c8PXBqEutguwvl0zDAF0AL0e2Z8WG9Dffjw==} 32 | dev: true 33 | 34 | /@esbuild-plugins/node-globals-polyfill@0.1.1(esbuild@0.16.3): 35 | resolution: {integrity: sha512-MR0oAA+mlnJWrt1RQVQ+4VYuRJW/P2YmRTv1AsplObyvuBMnPHiizUF95HHYiSsMGLhyGtWufaq2XQg6+iurBg==} 36 | peerDependencies: 37 | esbuild: '*' 38 | dependencies: 39 | esbuild: 0.16.3 40 | dev: true 41 | 42 | /@esbuild-plugins/node-modules-polyfill@0.1.4(esbuild@0.16.3): 43 | resolution: {integrity: sha512-uZbcXi0zbmKC/050p3gJnne5Qdzw8vkXIv+c2BW0Lsc1ji1SkrxbKPUy5Efr0blbTu1SL8w4eyfpnSdPg3G0Qg==} 44 | peerDependencies: 45 | esbuild: '*' 46 | dependencies: 47 | esbuild: 0.16.3 48 | escape-string-regexp: 4.0.0 49 | rollup-plugin-node-polyfills: 0.2.1 50 | dev: true 51 | 52 | /@esbuild/android-arm64@0.16.3: 53 | resolution: {integrity: sha512-RolFVeinkeraDvN/OoRf1F/lP0KUfGNb5jxy/vkIMeRRChkrX/HTYN6TYZosRJs3a1+8wqpxAo5PI5hFmxyPRg==} 54 | engines: {node: '>=12'} 55 | cpu: [arm64] 56 | os: [android] 57 | requiresBuild: true 58 | dev: true 59 | optional: true 60 | 61 | /@esbuild/android-arm@0.16.3: 62 | resolution: {integrity: sha512-mueuEoh+s1eRbSJqq9KNBQwI4QhQV6sRXIfTyLXSHGMpyew61rOK4qY21uKbXl1iBoMb0AdL1deWFCQVlN2qHA==} 63 | engines: {node: '>=12'} 64 | cpu: [arm] 65 | os: [android] 66 | requiresBuild: true 67 | dev: true 68 | optional: true 69 | 70 | /@esbuild/android-x64@0.16.3: 71 | resolution: {integrity: sha512-SFpTUcIT1bIJuCCBMCQWq1bL2gPTjWoLZdjmIhjdcQHaUfV41OQfho6Ici5uvvkMmZRXIUGpM3GxysP/EU7ifQ==} 72 | engines: {node: '>=12'} 73 | cpu: [x64] 74 | os: [android] 75 | requiresBuild: true 76 | dev: true 77 | optional: true 78 | 79 | /@esbuild/darwin-arm64@0.16.3: 80 | resolution: {integrity: sha512-DO8WykMyB+N9mIDfI/Hug70Dk1KipavlGAecxS3jDUwAbTpDXj0Lcwzw9svkhxfpCagDmpaTMgxWK8/C/XcXvw==} 81 | engines: {node: '>=12'} 82 | cpu: [arm64] 83 | os: [darwin] 84 | requiresBuild: true 85 | dev: true 86 | optional: true 87 | 88 | /@esbuild/darwin-x64@0.16.3: 89 | resolution: {integrity: sha512-uEqZQ2omc6BvWqdCiyZ5+XmxuHEi1SPzpVxXCSSV2+Sh7sbXbpeNhHIeFrIpRjAs0lI1FmA1iIOxFozKBhKgRQ==} 90 | engines: {node: '>=12'} 91 | cpu: [x64] 92 | os: [darwin] 93 | requiresBuild: true 94 | dev: true 95 | optional: true 96 | 97 | /@esbuild/freebsd-arm64@0.16.3: 98 | resolution: {integrity: sha512-nJansp3sSXakNkOD5i5mIz2Is/HjzIhFs49b1tjrPrpCmwgBmH9SSzhC/Z1UqlkivqMYkhfPwMw1dGFUuwmXhw==} 99 | engines: {node: '>=12'} 100 | cpu: [arm64] 101 | os: [freebsd] 102 | requiresBuild: true 103 | dev: true 104 | optional: true 105 | 106 | /@esbuild/freebsd-x64@0.16.3: 107 | resolution: {integrity: sha512-TfoDzLw+QHfc4a8aKtGSQ96Wa+6eimljjkq9HKR0rHlU83vw8aldMOUSJTUDxbcUdcgnJzPaX8/vGWm7vyV7ug==} 108 | engines: {node: '>=12'} 109 | cpu: [x64] 110 | os: [freebsd] 111 | requiresBuild: true 112 | dev: true 113 | optional: true 114 | 115 | /@esbuild/linux-arm64@0.16.3: 116 | resolution: {integrity: sha512-7I3RlsnxEFCHVZNBLb2w7unamgZ5sVwO0/ikE2GaYvYuUQs9Qte/w7TqWcXHtCwxvZx/2+F97ndiUQAWs47ZfQ==} 117 | engines: {node: '>=12'} 118 | cpu: [arm64] 119 | os: [linux] 120 | requiresBuild: true 121 | dev: true 122 | optional: true 123 | 124 | /@esbuild/linux-arm@0.16.3: 125 | resolution: {integrity: sha512-VwswmSYwVAAq6LysV59Fyqk3UIjbhuc6wb3vEcJ7HEJUtFuLK9uXWuFoH1lulEbE4+5GjtHi3MHX+w1gNHdOWQ==} 126 | engines: {node: '>=12'} 127 | cpu: [arm] 128 | os: [linux] 129 | requiresBuild: true 130 | dev: true 131 | optional: true 132 | 133 | /@esbuild/linux-ia32@0.16.3: 134 | resolution: {integrity: sha512-X8FDDxM9cqda2rJE+iblQhIMYY49LfvW4kaEjoFbTTQ4Go8G96Smj2w3BRTwA8IHGoi9dPOPGAX63dhuv19UqA==} 135 | engines: {node: '>=12'} 136 | cpu: [ia32] 137 | os: [linux] 138 | requiresBuild: true 139 | dev: true 140 | optional: true 141 | 142 | /@esbuild/linux-loong64@0.16.3: 143 | resolution: {integrity: sha512-hIbeejCOyO0X9ujfIIOKjBjNAs9XD/YdJ9JXAy1lHA+8UXuOqbFe4ErMCqMr8dhlMGBuvcQYGF7+kO7waj2KHw==} 144 | engines: {node: '>=12'} 145 | cpu: [loong64] 146 | os: [linux] 147 | requiresBuild: true 148 | dev: true 149 | optional: true 150 | 151 | /@esbuild/linux-mips64el@0.16.3: 152 | resolution: {integrity: sha512-znFRzICT/V8VZQMt6rjb21MtAVJv/3dmKRMlohlShrbVXdBuOdDrGb+C2cZGQAR8RFyRe7HS6klmHq103WpmVw==} 153 | engines: {node: '>=12'} 154 | cpu: [mips64el] 155 | os: [linux] 156 | requiresBuild: true 157 | dev: true 158 | optional: true 159 | 160 | /@esbuild/linux-ppc64@0.16.3: 161 | resolution: {integrity: sha512-EV7LuEybxhXrVTDpbqWF2yehYRNz5e5p+u3oQUS2+ZFpknyi1NXxr8URk4ykR8Efm7iu04//4sBg249yNOwy5Q==} 162 | engines: {node: '>=12'} 163 | cpu: [ppc64] 164 | os: [linux] 165 | requiresBuild: true 166 | dev: true 167 | optional: true 168 | 169 | /@esbuild/linux-riscv64@0.16.3: 170 | resolution: {integrity: sha512-uDxqFOcLzFIJ+r/pkTTSE9lsCEaV/Y6rMlQjUI9BkzASEChYL/aSQjZjchtEmdnVxDKETnUAmsaZ4pqK1eE5BQ==} 171 | engines: {node: '>=12'} 172 | cpu: [riscv64] 173 | os: [linux] 174 | requiresBuild: true 175 | dev: true 176 | optional: true 177 | 178 | /@esbuild/linux-s390x@0.16.3: 179 | resolution: {integrity: sha512-NbeREhzSxYwFhnCAQOQZmajsPYtX71Ufej3IQ8W2Gxskfz9DK58ENEju4SbpIj48VenktRASC52N5Fhyf/aliQ==} 180 | engines: {node: '>=12'} 181 | cpu: [s390x] 182 | os: [linux] 183 | requiresBuild: true 184 | dev: true 185 | optional: true 186 | 187 | /@esbuild/linux-x64@0.16.3: 188 | resolution: {integrity: sha512-SDiG0nCixYO9JgpehoKgScwic7vXXndfasjnD5DLbp1xltANzqZ425l7LSdHynt19UWOcDjG9wJJzSElsPvk0w==} 189 | engines: {node: '>=12'} 190 | cpu: [x64] 191 | os: [linux] 192 | requiresBuild: true 193 | dev: true 194 | optional: true 195 | 196 | /@esbuild/netbsd-x64@0.16.3: 197 | resolution: {integrity: sha512-AzbsJqiHEq1I/tUvOfAzCY15h4/7Ivp3ff/o1GpP16n48JMNAtbW0qui2WCgoIZArEHD0SUQ95gvR0oSO7ZbdA==} 198 | engines: {node: '>=12'} 199 | cpu: [x64] 200 | os: [netbsd] 201 | requiresBuild: true 202 | dev: true 203 | optional: true 204 | 205 | /@esbuild/openbsd-x64@0.16.3: 206 | resolution: {integrity: sha512-gSABi8qHl8k3Cbi/4toAzHiykuBuWLZs43JomTcXkjMZVkp0gj3gg9mO+9HJW/8GB5H89RX/V0QP4JGL7YEEVg==} 207 | engines: {node: '>=12'} 208 | cpu: [x64] 209 | os: [openbsd] 210 | requiresBuild: true 211 | dev: true 212 | optional: true 213 | 214 | /@esbuild/sunos-x64@0.16.3: 215 | resolution: {integrity: sha512-SF9Kch5Ete4reovvRO6yNjMxrvlfT0F0Flm+NPoUw5Z4Q3r1d23LFTgaLwm3Cp0iGbrU/MoUI+ZqwCv5XJijCw==} 216 | engines: {node: '>=12'} 217 | cpu: [x64] 218 | os: [sunos] 219 | requiresBuild: true 220 | dev: true 221 | optional: true 222 | 223 | /@esbuild/win32-arm64@0.16.3: 224 | resolution: {integrity: sha512-u5aBonZIyGopAZyOnoPAA6fGsDeHByZ9CnEzyML9NqntK6D/xl5jteZUKm/p6nD09+v3pTM6TuUIqSPcChk5gg==} 225 | engines: {node: '>=12'} 226 | cpu: [arm64] 227 | os: [win32] 228 | requiresBuild: true 229 | dev: true 230 | optional: true 231 | 232 | /@esbuild/win32-ia32@0.16.3: 233 | resolution: {integrity: sha512-GlgVq1WpvOEhNioh74TKelwla9KDuAaLZrdxuuUgsP2vayxeLgVc+rbpIv0IYF4+tlIzq2vRhofV+KGLD+37EQ==} 234 | engines: {node: '>=12'} 235 | cpu: [ia32] 236 | os: [win32] 237 | requiresBuild: true 238 | dev: true 239 | optional: true 240 | 241 | /@esbuild/win32-x64@0.16.3: 242 | resolution: {integrity: sha512-5/JuTd8OWW8UzEtyf19fbrtMJENza+C9JoPIkvItgTBQ1FO2ZLvjbPO6Xs54vk0s5JB5QsfieUEshRQfu7ZHow==} 243 | engines: {node: '>=12'} 244 | cpu: [x64] 245 | os: [win32] 246 | requiresBuild: true 247 | dev: true 248 | optional: true 249 | 250 | /@iarna/toml@2.2.5: 251 | resolution: {integrity: sha512-trnsAYxU3xnS1gPHPyU961coFyLkh4gAD/0zQ5mymY4yOZ+CYvsPqUbOFSw0aDM4y0tV7tiFxL/1XfXPNC6IPg==} 252 | dev: true 253 | 254 | /@miniflare/cache@2.13.0: 255 | resolution: {integrity: sha512-y3SdN3SVyPECWmLAEGkkrv0RB+LugEPs/FeXn8QtN9aE1vyj69clOAgmsDzoh1DpFfFsLKRiv05aWs4m79P8Xw==} 256 | engines: {node: '>=16.13'} 257 | dependencies: 258 | '@miniflare/core': 2.13.0 259 | '@miniflare/shared': 2.13.0 260 | http-cache-semantics: 4.1.0 261 | undici: 5.20.0 262 | dev: true 263 | 264 | /@miniflare/cli-parser@2.13.0: 265 | resolution: {integrity: sha512-Nx1PIfuMZ3mK9Dg/JojWZAjHR16h1pcdCFSqYln/ME7y5ifx+P1E5UkShWUQ1cBlibNaltjbJ2n/7stSAsIGPQ==} 266 | engines: {node: '>=16.13'} 267 | dependencies: 268 | '@miniflare/shared': 2.13.0 269 | kleur: 4.1.4 270 | dev: true 271 | 272 | /@miniflare/core@2.13.0: 273 | resolution: {integrity: sha512-YJ/C0J3k+7xn4gvlMpvePnM3xC8nOnkweW96cc0IA8kJ1JSmScOO2tZ7rrU1RyDgp6StkAtQBw4yC0wYeFycBw==} 274 | engines: {node: '>=16.13'} 275 | dependencies: 276 | '@iarna/toml': 2.2.5 277 | '@miniflare/queues': 2.13.0 278 | '@miniflare/shared': 2.13.0 279 | '@miniflare/watcher': 2.13.0 280 | busboy: 1.6.0 281 | dotenv: 10.0.0 282 | kleur: 4.1.4 283 | set-cookie-parser: 2.4.8 284 | undici: 5.20.0 285 | urlpattern-polyfill: 4.0.3 286 | dev: true 287 | 288 | /@miniflare/d1@2.13.0: 289 | resolution: {integrity: sha512-OslqjO8iTcvzyrC0spByftMboRmHJEyHyTHnlKkjWDGdQQztEOjso2Xj+3I4SZIeUYvbzDRhKLS2QXI9a8LS5A==} 290 | engines: {node: '>=16.7'} 291 | dependencies: 292 | '@miniflare/core': 2.13.0 293 | '@miniflare/shared': 2.13.0 294 | dev: true 295 | 296 | /@miniflare/durable-objects@2.13.0: 297 | resolution: {integrity: sha512-CRGVBPO9vY4Fc3aV+pdPRVVeYIt64vQqvw+BJbyW+TQtqVP2CGQeziJGnCfcONNNKyooZxGyUkHewUypyH+Qhg==} 298 | engines: {node: '>=16.13'} 299 | dependencies: 300 | '@miniflare/core': 2.13.0 301 | '@miniflare/shared': 2.13.0 302 | '@miniflare/storage-memory': 2.13.0 303 | undici: 5.20.0 304 | dev: true 305 | 306 | /@miniflare/html-rewriter@2.13.0: 307 | resolution: {integrity: sha512-XhN7Icyzvtvu+o/A0hrnSiSmla78seCaNwQ9M1TDHxt352I/ahPX4wtPXs6GbKqY0/i+V6yoG2KGFRQ/j59cQQ==} 308 | engines: {node: '>=16.13'} 309 | dependencies: 310 | '@miniflare/core': 2.13.0 311 | '@miniflare/shared': 2.13.0 312 | html-rewriter-wasm: 0.4.1 313 | undici: 5.20.0 314 | dev: true 315 | 316 | /@miniflare/http-server@2.13.0: 317 | resolution: {integrity: sha512-aMS/nUMTKP15hKnyZboeuWCiqmNrrCu+XRBY/TxDDl07iXcLpiHGf3oVv+yXxXkWlJHJVCbK7i/nXSNPllRMSw==} 318 | engines: {node: '>=16.13'} 319 | dependencies: 320 | '@miniflare/core': 2.13.0 321 | '@miniflare/shared': 2.13.0 322 | '@miniflare/web-sockets': 2.13.0 323 | kleur: 4.1.4 324 | selfsigned: 2.0.1 325 | undici: 5.20.0 326 | ws: 8.6.0 327 | youch: 2.2.2 328 | transitivePeerDependencies: 329 | - bufferutil 330 | - utf-8-validate 331 | dev: true 332 | 333 | /@miniflare/kv@2.13.0: 334 | resolution: {integrity: sha512-J0AS5x3g/YVOmHMxMAZs07nRXRvSo9jyuC0eikTBf+4AABvBIyvVYmdTjYNjCmr8O5smcfWBX5S27HelD3aAAQ==} 335 | engines: {node: '>=16.13'} 336 | dependencies: 337 | '@miniflare/shared': 2.13.0 338 | dev: true 339 | 340 | /@miniflare/queues@2.13.0: 341 | resolution: {integrity: sha512-Gf/a6M1mJL03iOvNqh3JNahcBfvEMPHnO28n0gkCoyYWGvddIr9lwCdFIa0qwNJsC1fIDRxhPg8PZ5cQLBMwRA==} 342 | engines: {node: '>=16.7'} 343 | dependencies: 344 | '@miniflare/shared': 2.13.0 345 | dev: true 346 | 347 | /@miniflare/r2@2.13.0: 348 | resolution: {integrity: sha512-/5k6GHOYMNV/oBtilV9HDXBkJUrx8oXVigG5vxbnzEGRXyVRmR+Glzu7mFT8JiE94XiEbXHk9Qvu1S5Dej3wBw==} 349 | engines: {node: '>=16.13'} 350 | dependencies: 351 | '@miniflare/shared': 2.13.0 352 | undici: 5.20.0 353 | dev: true 354 | 355 | /@miniflare/runner-vm@2.13.0: 356 | resolution: {integrity: sha512-VmKtF2cA8HmTuLXor1THWY0v+DmaobPct63iLcgWIaUdP3MIvL+9X8HDXFAviCR7bCTe6MKxckHkaOj0IE0aJQ==} 357 | engines: {node: '>=16.13'} 358 | dependencies: 359 | '@miniflare/shared': 2.13.0 360 | dev: true 361 | 362 | /@miniflare/scheduler@2.13.0: 363 | resolution: {integrity: sha512-AOaQanoR4NjVEzVGWHnrL15A7aMx+d9AKLJhSDF7KaP+4NrT2Wo2BQuXCpn5oStx3itOdlQpMfqQ139e/I8WhQ==} 364 | engines: {node: '>=16.13'} 365 | dependencies: 366 | '@miniflare/core': 2.13.0 367 | '@miniflare/shared': 2.13.0 368 | cron-schedule: 3.0.6 369 | dev: true 370 | 371 | /@miniflare/shared@2.13.0: 372 | resolution: {integrity: sha512-m8YFQzKmbjberrV9hPzNcQjNCXxjTjXUpuNrIGjAJO7g+BDztUHaZbdd26H9maBDlkeiWxA3hf0mDyCT/6MCMA==} 373 | engines: {node: '>=16.13'} 374 | dependencies: 375 | '@types/better-sqlite3': 7.6.3 376 | kleur: 4.1.4 377 | npx-import: 1.1.4 378 | picomatch: 2.3.1 379 | dev: true 380 | 381 | /@miniflare/sites@2.13.0: 382 | resolution: {integrity: sha512-/tuzIu00o6CF2tkSv01q02MgEShXBSKx85h9jwWvc+6u7prGacAOer0FA1YNRFbE+t9QIfutAkoPGMA9zYf8+Q==} 383 | engines: {node: '>=16.13'} 384 | dependencies: 385 | '@miniflare/kv': 2.13.0 386 | '@miniflare/shared': 2.13.0 387 | '@miniflare/storage-file': 2.13.0 388 | dev: true 389 | 390 | /@miniflare/storage-file@2.13.0: 391 | resolution: {integrity: sha512-LuAeAAY5046rq5U1eFLVkz+ppiFEWytWacpkQw92DvVKFFquZcXSj6WPxZF4rSs23WDk+rdcwuLekbb52aDR7A==} 392 | engines: {node: '>=16.13'} 393 | dependencies: 394 | '@miniflare/shared': 2.13.0 395 | '@miniflare/storage-memory': 2.13.0 396 | dev: true 397 | 398 | /@miniflare/storage-memory@2.13.0: 399 | resolution: {integrity: sha512-FnkYcBNXa/ym1ksNilNZycg9WYYKo6cWKplVBeSthRon3e8QY6t3n7/XRseBUo7O6mhDybVTy4wNCP1R2nBiEw==} 400 | engines: {node: '>=16.13'} 401 | dependencies: 402 | '@miniflare/shared': 2.13.0 403 | dev: true 404 | 405 | /@miniflare/watcher@2.13.0: 406 | resolution: {integrity: sha512-teAacWcpMStoBLbLae95IUaL5lPzjPlXa9lhK9CbRaio/KRMibTMRGWrYos3IVGQRZvklvLwcms/nTvgcdb6yw==} 407 | engines: {node: '>=16.13'} 408 | dependencies: 409 | '@miniflare/shared': 2.13.0 410 | dev: true 411 | 412 | /@miniflare/web-sockets@2.13.0: 413 | resolution: {integrity: sha512-+U2/HCf+BetRIgjAnNQjkuN6UeAjQmXifhQC+7CCaX834XJhrKXoR6z2xr2xkg1qj0qQs4D2jWG0KzrO5OUpug==} 414 | engines: {node: '>=16.13'} 415 | dependencies: 416 | '@miniflare/core': 2.13.0 417 | '@miniflare/shared': 2.13.0 418 | undici: 5.20.0 419 | ws: 8.6.0 420 | transitivePeerDependencies: 421 | - bufferutil 422 | - utf-8-validate 423 | dev: true 424 | 425 | /@types/better-sqlite3@7.6.3: 426 | resolution: {integrity: sha512-YS64N9SNDT/NAvou3QNdzAu3E2om/W/0dhORimtPGLef+zSK5l1vDzfsWb4xgXOgfhtOI5ZDTRxnvRPb22AIVQ==} 427 | dependencies: 428 | '@types/node': 18.11.18 429 | dev: true 430 | 431 | /@types/node@18.11.18: 432 | resolution: {integrity: sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA==} 433 | dev: true 434 | 435 | /@types/range-parser@1.2.4: 436 | resolution: {integrity: sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==} 437 | dev: true 438 | 439 | /@types/stack-trace@0.0.29: 440 | resolution: {integrity: sha512-TgfOX+mGY/NyNxJLIbDWrO9DjGoVSW9+aB8H2yy1fy32jsvxijhmyJI9fDFgvz3YP4lvJaq9DzdR/M1bOgVc9g==} 441 | dev: true 442 | 443 | /anymatch@3.1.2: 444 | resolution: {integrity: sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==} 445 | engines: {node: '>= 8'} 446 | dependencies: 447 | normalize-path: 3.0.0 448 | picomatch: 2.3.1 449 | dev: true 450 | 451 | /binary-extensions@2.2.0: 452 | resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==} 453 | engines: {node: '>=8'} 454 | dev: true 455 | 456 | /blake3-wasm@2.1.5: 457 | resolution: {integrity: sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==} 458 | dev: true 459 | 460 | /braces@3.0.2: 461 | resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==} 462 | engines: {node: '>=8'} 463 | dependencies: 464 | fill-range: 7.0.1 465 | dev: true 466 | 467 | /buffer-from@1.1.2: 468 | resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} 469 | dev: true 470 | 471 | /builtins@5.0.1: 472 | resolution: {integrity: sha512-qwVpFEHNfhYJIzNRBvd2C1kyo6jz3ZSMPyyuR47OPdiKWlbYnZNyDWuyR175qDnAJLiCo5fBBqPb3RiXgWlkOQ==} 473 | dependencies: 474 | semver: 7.3.8 475 | dev: true 476 | 477 | /busboy@1.6.0: 478 | resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} 479 | engines: {node: '>=10.16.0'} 480 | dependencies: 481 | streamsearch: 1.1.0 482 | dev: true 483 | 484 | /chokidar@3.5.3: 485 | resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==} 486 | engines: {node: '>= 8.10.0'} 487 | dependencies: 488 | anymatch: 3.1.2 489 | braces: 3.0.2 490 | glob-parent: 5.1.2 491 | is-binary-path: 2.1.0 492 | is-glob: 4.0.3 493 | normalize-path: 3.0.0 494 | readdirp: 3.6.0 495 | optionalDependencies: 496 | fsevents: 2.3.2 497 | dev: true 498 | 499 | /cookie@0.4.2: 500 | resolution: {integrity: sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==} 501 | engines: {node: '>= 0.6'} 502 | dev: true 503 | 504 | /cron-schedule@3.0.6: 505 | resolution: {integrity: sha512-izfGgKyzzIyLaeb1EtZ3KbglkS6AKp9cv7LxmiyoOu+fXfol1tQDC0Cof0enVZGNtudTHW+3lfuW9ZkLQss4Wg==} 506 | dev: true 507 | 508 | /cross-spawn@7.0.3: 509 | resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} 510 | engines: {node: '>= 8'} 511 | dependencies: 512 | path-key: 3.1.1 513 | shebang-command: 2.0.0 514 | which: 2.0.2 515 | dev: true 516 | 517 | /dotenv@10.0.0: 518 | resolution: {integrity: sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==} 519 | engines: {node: '>=10'} 520 | dev: true 521 | 522 | /esbuild@0.16.3: 523 | resolution: {integrity: sha512-71f7EjPWTiSguen8X/kxEpkAS7BFHwtQKisCDDV3Y4GLGWBaoSCyD5uXkaUew6JDzA9FEN1W23mdnSwW9kqCeg==} 524 | engines: {node: '>=12'} 525 | hasBin: true 526 | requiresBuild: true 527 | optionalDependencies: 528 | '@esbuild/android-arm': 0.16.3 529 | '@esbuild/android-arm64': 0.16.3 530 | '@esbuild/android-x64': 0.16.3 531 | '@esbuild/darwin-arm64': 0.16.3 532 | '@esbuild/darwin-x64': 0.16.3 533 | '@esbuild/freebsd-arm64': 0.16.3 534 | '@esbuild/freebsd-x64': 0.16.3 535 | '@esbuild/linux-arm': 0.16.3 536 | '@esbuild/linux-arm64': 0.16.3 537 | '@esbuild/linux-ia32': 0.16.3 538 | '@esbuild/linux-loong64': 0.16.3 539 | '@esbuild/linux-mips64el': 0.16.3 540 | '@esbuild/linux-ppc64': 0.16.3 541 | '@esbuild/linux-riscv64': 0.16.3 542 | '@esbuild/linux-s390x': 0.16.3 543 | '@esbuild/linux-x64': 0.16.3 544 | '@esbuild/netbsd-x64': 0.16.3 545 | '@esbuild/openbsd-x64': 0.16.3 546 | '@esbuild/sunos-x64': 0.16.3 547 | '@esbuild/win32-arm64': 0.16.3 548 | '@esbuild/win32-ia32': 0.16.3 549 | '@esbuild/win32-x64': 0.16.3 550 | dev: true 551 | 552 | /escape-string-regexp@4.0.0: 553 | resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} 554 | engines: {node: '>=10'} 555 | dev: true 556 | 557 | /estree-walker@0.6.1: 558 | resolution: {integrity: sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==} 559 | dev: true 560 | 561 | /execa@6.1.0: 562 | resolution: {integrity: sha512-QVWlX2e50heYJcCPG0iWtf8r0xjEYfz/OYLGDYH+IyjWezzPNxz63qNFOu0l4YftGWuizFVZHHs8PrLU5p2IDA==} 563 | engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} 564 | dependencies: 565 | cross-spawn: 7.0.3 566 | get-stream: 6.0.1 567 | human-signals: 3.0.1 568 | is-stream: 3.0.0 569 | merge-stream: 2.0.0 570 | npm-run-path: 5.1.0 571 | onetime: 6.0.0 572 | signal-exit: 3.0.7 573 | strip-final-newline: 3.0.0 574 | dev: true 575 | 576 | /fill-range@7.0.1: 577 | resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==} 578 | engines: {node: '>=8'} 579 | dependencies: 580 | to-regex-range: 5.0.1 581 | dev: true 582 | 583 | /fsevents@2.3.2: 584 | resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} 585 | engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} 586 | os: [darwin] 587 | requiresBuild: true 588 | dev: true 589 | optional: true 590 | 591 | /get-stream@6.0.1: 592 | resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} 593 | engines: {node: '>=10'} 594 | dev: true 595 | 596 | /glob-parent@5.1.2: 597 | resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} 598 | engines: {node: '>= 6'} 599 | dependencies: 600 | is-glob: 4.0.3 601 | dev: true 602 | 603 | /html-rewriter-wasm@0.4.1: 604 | resolution: {integrity: sha512-lNovG8CMCCmcVB1Q7xggMSf7tqPCijZXaH4gL6iE8BFghdQCbaY5Met9i1x2Ex8m/cZHDUtXK9H6/znKamRP8Q==} 605 | dev: true 606 | 607 | /http-cache-semantics@4.1.0: 608 | resolution: {integrity: sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==} 609 | dev: true 610 | 611 | /human-signals@3.0.1: 612 | resolution: {integrity: sha512-rQLskxnM/5OCldHo+wNXbpVgDn5A17CUoKX+7Sokwaknlq7CdSnphy0W39GU8dw59XiCXmFXDg4fRuckQRKewQ==} 613 | engines: {node: '>=12.20.0'} 614 | dev: true 615 | 616 | /is-binary-path@2.1.0: 617 | resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} 618 | engines: {node: '>=8'} 619 | dependencies: 620 | binary-extensions: 2.2.0 621 | dev: true 622 | 623 | /is-extglob@2.1.1: 624 | resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} 625 | engines: {node: '>=0.10.0'} 626 | dev: true 627 | 628 | /is-glob@4.0.3: 629 | resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} 630 | engines: {node: '>=0.10.0'} 631 | dependencies: 632 | is-extglob: 2.1.1 633 | dev: true 634 | 635 | /is-number@7.0.0: 636 | resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} 637 | engines: {node: '>=0.12.0'} 638 | dev: true 639 | 640 | /is-stream@3.0.0: 641 | resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==} 642 | engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} 643 | dev: true 644 | 645 | /isexe@2.0.0: 646 | resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} 647 | dev: true 648 | 649 | /kleur@4.1.4: 650 | resolution: {integrity: sha512-8QADVssbrFjivHWQU7KkMgptGTl6WAcSdlbBPY4uNF+mWr6DGcKrvY2w4FQJoXch7+fKMjj0dRrL75vk3k23OA==} 651 | engines: {node: '>=6'} 652 | dev: true 653 | 654 | /lru-cache@6.0.0: 655 | resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} 656 | engines: {node: '>=10'} 657 | dependencies: 658 | yallist: 4.0.0 659 | dev: true 660 | 661 | /magic-string@0.25.9: 662 | resolution: {integrity: sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==} 663 | dependencies: 664 | sourcemap-codec: 1.4.8 665 | dev: true 666 | 667 | /merge-stream@2.0.0: 668 | resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} 669 | dev: true 670 | 671 | /mime@3.0.0: 672 | resolution: {integrity: sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==} 673 | engines: {node: '>=10.0.0'} 674 | hasBin: true 675 | dev: true 676 | 677 | /mimic-fn@4.0.0: 678 | resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==} 679 | engines: {node: '>=12'} 680 | dev: true 681 | 682 | /miniflare@2.13.0: 683 | resolution: {integrity: sha512-ayNhVa4a6bZiOuHtrPmOt4BCYcmW1fBQ/+qGL85smq1m2OBBm3aUs6f4ISf38xH8tk+qewgmAywetyVtn6KHPw==} 684 | engines: {node: '>=16.13'} 685 | hasBin: true 686 | peerDependencies: 687 | '@miniflare/storage-redis': 2.13.0 688 | cron-schedule: ^3.0.4 689 | ioredis: ^4.27.9 690 | peerDependenciesMeta: 691 | '@miniflare/storage-redis': 692 | optional: true 693 | cron-schedule: 694 | optional: true 695 | ioredis: 696 | optional: true 697 | dependencies: 698 | '@miniflare/cache': 2.13.0 699 | '@miniflare/cli-parser': 2.13.0 700 | '@miniflare/core': 2.13.0 701 | '@miniflare/d1': 2.13.0 702 | '@miniflare/durable-objects': 2.13.0 703 | '@miniflare/html-rewriter': 2.13.0 704 | '@miniflare/http-server': 2.13.0 705 | '@miniflare/kv': 2.13.0 706 | '@miniflare/queues': 2.13.0 707 | '@miniflare/r2': 2.13.0 708 | '@miniflare/runner-vm': 2.13.0 709 | '@miniflare/scheduler': 2.13.0 710 | '@miniflare/shared': 2.13.0 711 | '@miniflare/sites': 2.13.0 712 | '@miniflare/storage-file': 2.13.0 713 | '@miniflare/storage-memory': 2.13.0 714 | '@miniflare/web-sockets': 2.13.0 715 | kleur: 4.1.4 716 | semiver: 1.1.0 717 | source-map-support: 0.5.21 718 | undici: 5.20.0 719 | transitivePeerDependencies: 720 | - bufferutil 721 | - utf-8-validate 722 | dev: true 723 | 724 | /mustache@4.2.0: 725 | resolution: {integrity: sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==} 726 | hasBin: true 727 | dev: true 728 | 729 | /nanoid@3.3.4: 730 | resolution: {integrity: sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==} 731 | engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} 732 | hasBin: true 733 | dev: true 734 | 735 | /node-forge@1.3.1: 736 | resolution: {integrity: sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==} 737 | engines: {node: '>= 6.13.0'} 738 | dev: true 739 | 740 | /normalize-path@3.0.0: 741 | resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} 742 | engines: {node: '>=0.10.0'} 743 | dev: true 744 | 745 | /npm-run-path@5.1.0: 746 | resolution: {integrity: sha512-sJOdmRGrY2sjNTRMbSvluQqg+8X7ZK61yvzBEIDhz4f8z1TZFYABsqjjCBd/0PUNE9M6QDgHJXQkGUEm7Q+l9Q==} 747 | engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} 748 | dependencies: 749 | path-key: 4.0.0 750 | dev: true 751 | 752 | /npx-import@1.1.4: 753 | resolution: {integrity: sha512-3ShymTWOgqGyNlh5lMJAejLuIv3W1K3fbI5Ewc6YErZU3Sp0PqsNs8UIU1O8z5+KVl/Du5ag56Gza9vdorGEoA==} 754 | dependencies: 755 | execa: 6.1.0 756 | parse-package-name: 1.0.0 757 | semver: 7.3.8 758 | validate-npm-package-name: 4.0.0 759 | dev: true 760 | 761 | /onetime@6.0.0: 762 | resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==} 763 | engines: {node: '>=12'} 764 | dependencies: 765 | mimic-fn: 4.0.0 766 | dev: true 767 | 768 | /parse-package-name@1.0.0: 769 | resolution: {integrity: sha512-kBeTUtcj+SkyfaW4+KBe0HtsloBJ/mKTPoxpVdA57GZiPerREsUWJOhVj9anXweFiJkm5y8FG1sxFZkZ0SN6wg==} 770 | dev: true 771 | 772 | /path-key@3.1.1: 773 | resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} 774 | engines: {node: '>=8'} 775 | dev: true 776 | 777 | /path-key@4.0.0: 778 | resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} 779 | engines: {node: '>=12'} 780 | dev: true 781 | 782 | /path-to-regexp@6.2.1: 783 | resolution: {integrity: sha512-JLyh7xT1kizaEvcaXOQwOc2/Yhw6KZOvPf1S8401UyLk86CU79LN3vl7ztXGm/pZ+YjoyAJ4rxmHwbkBXJX+yw==} 784 | dev: true 785 | 786 | /picomatch@2.3.1: 787 | resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} 788 | engines: {node: '>=8.6'} 789 | dev: true 790 | 791 | /range-parser@1.2.1: 792 | resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} 793 | engines: {node: '>= 0.6'} 794 | dev: false 795 | 796 | /readdirp@3.6.0: 797 | resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} 798 | engines: {node: '>=8.10.0'} 799 | dependencies: 800 | picomatch: 2.3.1 801 | dev: true 802 | 803 | /rollup-plugin-inject@3.0.2: 804 | resolution: {integrity: sha512-ptg9PQwzs3orn4jkgXJ74bfs5vYz1NCZlSQMBUA0wKcGp5i5pA1AO3fOUEte8enhGUC+iapTCzEWw2jEFFUO/w==} 805 | deprecated: This package has been deprecated and is no longer maintained. Please use @rollup/plugin-inject. 806 | dependencies: 807 | estree-walker: 0.6.1 808 | magic-string: 0.25.9 809 | rollup-pluginutils: 2.8.2 810 | dev: true 811 | 812 | /rollup-plugin-node-polyfills@0.2.1: 813 | resolution: {integrity: sha512-4kCrKPTJ6sK4/gLL/U5QzVT8cxJcofO0OU74tnB19F40cmuAKSzH5/siithxlofFEjwvw1YAhPmbvGNA6jEroA==} 814 | dependencies: 815 | rollup-plugin-inject: 3.0.2 816 | dev: true 817 | 818 | /rollup-pluginutils@2.8.2: 819 | resolution: {integrity: sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ==} 820 | dependencies: 821 | estree-walker: 0.6.1 822 | dev: true 823 | 824 | /selfsigned@2.0.1: 825 | resolution: {integrity: sha512-LmME957M1zOsUhG+67rAjKfiWFox3SBxE/yymatMZsAx+oMrJ0YQ8AToOnyCm7xbeg2ep37IHLxdu0o2MavQOQ==} 826 | engines: {node: '>=10'} 827 | dependencies: 828 | node-forge: 1.3.1 829 | dev: true 830 | 831 | /semiver@1.1.0: 832 | resolution: {integrity: sha512-QNI2ChmuioGC1/xjyYwyZYADILWyW6AmS1UH6gDj/SFUUUS4MBAWs/7mxnkRPc/F4iHezDP+O8t0dO8WHiEOdg==} 833 | engines: {node: '>=6'} 834 | dev: true 835 | 836 | /semver@7.3.8: 837 | resolution: {integrity: sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==} 838 | engines: {node: '>=10'} 839 | hasBin: true 840 | dependencies: 841 | lru-cache: 6.0.0 842 | dev: true 843 | 844 | /set-cookie-parser@2.4.8: 845 | resolution: {integrity: sha512-edRH8mBKEWNVIVMKejNnuJxleqYE/ZSdcT8/Nem9/mmosx12pctd80s2Oy00KNZzrogMZS5mauK2/ymL1bvlvg==} 846 | dev: true 847 | 848 | /shebang-command@2.0.0: 849 | resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} 850 | engines: {node: '>=8'} 851 | dependencies: 852 | shebang-regex: 3.0.0 853 | dev: true 854 | 855 | /shebang-regex@3.0.0: 856 | resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} 857 | engines: {node: '>=8'} 858 | dev: true 859 | 860 | /signal-exit@3.0.7: 861 | resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} 862 | dev: true 863 | 864 | /source-map-support@0.5.21: 865 | resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} 866 | dependencies: 867 | buffer-from: 1.1.2 868 | source-map: 0.6.1 869 | dev: true 870 | 871 | /source-map@0.6.1: 872 | resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} 873 | engines: {node: '>=0.10.0'} 874 | dev: true 875 | 876 | /source-map@0.7.4: 877 | resolution: {integrity: sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==} 878 | engines: {node: '>= 8'} 879 | dev: true 880 | 881 | /sourcemap-codec@1.4.8: 882 | resolution: {integrity: sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==} 883 | deprecated: Please use @jridgewell/sourcemap-codec instead 884 | dev: true 885 | 886 | /stack-trace@0.0.10: 887 | resolution: {integrity: sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==} 888 | dev: true 889 | 890 | /streamsearch@1.1.0: 891 | resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} 892 | engines: {node: '>=10.0.0'} 893 | dev: true 894 | 895 | /strip-final-newline@3.0.0: 896 | resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==} 897 | engines: {node: '>=12'} 898 | dev: true 899 | 900 | /to-regex-range@5.0.1: 901 | resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} 902 | engines: {node: '>=8.0'} 903 | dependencies: 904 | is-number: 7.0.0 905 | dev: true 906 | 907 | /typescript@4.7.2: 908 | resolution: {integrity: sha512-Mamb1iX2FDUpcTRzltPxgWMKy3fhg0TN378ylbktPGPK/99KbDtMQ4W1hwgsbPAsG3a0xKa1vmw4VKZQbkvz5A==} 909 | engines: {node: '>=4.2.0'} 910 | hasBin: true 911 | dev: true 912 | 913 | /undici@5.20.0: 914 | resolution: {integrity: sha512-J3j60dYzuo6Eevbawwp1sdg16k5Tf768bxYK4TUJRH7cBM4kFCbf3mOnM/0E3vQYXvpxITbbWmBafaDbxLDz3g==} 915 | engines: {node: '>=12.18'} 916 | dependencies: 917 | busboy: 1.6.0 918 | dev: true 919 | 920 | /urlpattern-polyfill@4.0.3: 921 | resolution: {integrity: sha512-DOE84vZT2fEcl9gqCUTcnAw5ZY5Id55ikUcziSUntuEFL3pRvavg5kwDmTEUJkeCHInTlV/HexFomgYnzO5kdQ==} 922 | dev: true 923 | 924 | /validate-npm-package-name@4.0.0: 925 | resolution: {integrity: sha512-mzR0L8ZDktZjpX4OB46KT+56MAhl4EIazWP/+G/HPGuvfdaqg4YsCdtOm6U9+LOFyYDoh4dpnpxZRB9MQQns5Q==} 926 | engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} 927 | dependencies: 928 | builtins: 5.0.1 929 | dev: true 930 | 931 | /which@2.0.2: 932 | resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} 933 | engines: {node: '>= 8'} 934 | hasBin: true 935 | dependencies: 936 | isexe: 2.0.0 937 | dev: true 938 | 939 | /wrangler@2.20.0: 940 | resolution: {integrity: sha512-UdKJ2LD7qgDxDvll/GkR1HnRP+bcEdqi/HJjDI+7eF4lv9V940jmm3orxCkSEosGyE14q0q6dBRM95+fBI8tdQ==} 941 | engines: {node: '>=16.13.0'} 942 | hasBin: true 943 | dependencies: 944 | '@cloudflare/kv-asset-handler': 0.2.0 945 | '@esbuild-plugins/node-globals-polyfill': 0.1.1(esbuild@0.16.3) 946 | '@esbuild-plugins/node-modules-polyfill': 0.1.4(esbuild@0.16.3) 947 | '@miniflare/core': 2.13.0 948 | '@miniflare/d1': 2.13.0 949 | '@miniflare/durable-objects': 2.13.0 950 | blake3-wasm: 2.1.5 951 | chokidar: 3.5.3 952 | esbuild: 0.16.3 953 | miniflare: 2.13.0 954 | nanoid: 3.3.4 955 | path-to-regexp: 6.2.1 956 | selfsigned: 2.0.1 957 | source-map: 0.7.4 958 | xxhash-wasm: 1.0.1 959 | optionalDependencies: 960 | fsevents: 2.3.2 961 | transitivePeerDependencies: 962 | - '@miniflare/storage-redis' 963 | - bufferutil 964 | - cron-schedule 965 | - ioredis 966 | - utf-8-validate 967 | dev: true 968 | 969 | /ws@8.6.0: 970 | resolution: {integrity: sha512-AzmM3aH3gk0aX7/rZLYvjdvZooofDu3fFOzGqcSnQ1tOcTWwhM/o+q++E8mAyVVIyUdajrkzWUGftaVSDLn1bw==} 971 | engines: {node: '>=10.0.0'} 972 | peerDependencies: 973 | bufferutil: ^4.0.1 974 | utf-8-validate: ^5.0.2 975 | peerDependenciesMeta: 976 | bufferutil: 977 | optional: true 978 | utf-8-validate: 979 | optional: true 980 | dev: true 981 | 982 | /xxhash-wasm@1.0.1: 983 | resolution: {integrity: sha512-Lc9CTvDrH2vRoiaUzz25q7lRaviMhz90pkx6YxR9EPYtF99yOJnv2cB+CQ0hp/TLoqrUsk8z/W2EN31T568Azw==} 984 | dev: true 985 | 986 | /yallist@4.0.0: 987 | resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} 988 | dev: true 989 | 990 | /youch@2.2.2: 991 | resolution: {integrity: sha512-/FaCeG3GkuJwaMR34GHVg0l8jCbafZLHiFowSjqLlqhC6OMyf2tPJBu8UirF7/NI9X/R5ai4QfEKUCOxMAGxZQ==} 992 | dependencies: 993 | '@types/stack-trace': 0.0.29 994 | cookie: 0.4.2 995 | mustache: 4.2.0 996 | stack-trace: 0.0.10 997 | dev: true 998 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import parseRange from "range-parser"; 2 | 3 | export interface Env { 4 | R2_BUCKET: R2Bucket, 5 | ALLOWED_ORIGINS?: string, 6 | CACHE_CONTROL?: string, 7 | PATH_PREFIX?: string 8 | INDEX_FILE?: string 9 | NOTFOUND_FILE?: string 10 | DIRECTORY_LISTING?: boolean 11 | HIDE_HIDDEN_FILES?: boolean 12 | DIRECTORY_CACHE_CONTROL?: string 13 | } 14 | 15 | const units = ['B', 'KB', 'MB', 'GB', 'TB']; 16 | 17 | type ParsedRange = { offset: number, length: number } | { suffix: number }; 18 | 19 | function rangeHasLength(object: ParsedRange): object is { offset: number, length: number } { 20 | return (<{ offset: number, length: number }>object).length !== undefined; 21 | } 22 | 23 | function hasBody(object: R2Object | R2ObjectBody): object is R2ObjectBody { 24 | return (object).body !== undefined; 25 | } 26 | 27 | function hasSuffix(range: ParsedRange): range is { suffix: number } { 28 | return (<{ suffix: number }>range).suffix !== undefined; 29 | } 30 | 31 | function getRangeHeader(range: ParsedRange, fileSize: number): string { 32 | return `bytes ${hasSuffix(range) ? (fileSize - range.suffix) : range.offset}-${hasSuffix(range) ? fileSize - 1 : 33 | (range.offset + range.length - 1)}/${fileSize}`; 34 | } 35 | 36 | // some ideas for this were taken from / inspired by 37 | // https://github.com/cloudflare/workerd/blob/main/samples/static-files-from-disk/static.js 38 | async function makeListingResponse(path: string, env: Env, request: Request): Promise { 39 | if (path === "/") 40 | path = "" 41 | else if (path !== "" && !path.endsWith("/")) { 42 | path += "/"; 43 | } 44 | let listing = await env.R2_BUCKET.list({ prefix: path, delimiter: '/' }) 45 | 46 | if (listing.delimitedPrefixes.length === 0 && listing.objects.length === 0) { 47 | return null; 48 | } 49 | 50 | let html: string = ""; 51 | let lastModified: Date | null = null; 52 | 53 | if (request.method === "GET") { 54 | let htmlList = []; 55 | 56 | if (path !== "") { 57 | htmlList.push( 58 | ` ` + 59 | `../` + 60 | `--`); 61 | } 62 | 63 | for (let dir of listing.delimitedPrefixes) { 64 | if (dir.endsWith("/")) dir = dir.substring(0, dir.length - 1) 65 | let name = dir.substring(path.length, dir.length) 66 | if (name.startsWith(".") && env.HIDE_HIDDEN_FILES) continue; 67 | htmlList.push( 68 | ` ` + 69 | `${name}/` + 70 | `--`); 71 | } 72 | for (let file of listing.objects) { 73 | let name = file.key.substring(path.length, file.key.length) 74 | if (name.startsWith(".") && env.HIDE_HIDDEN_FILES) continue; 75 | 76 | let dateStr = file.uploaded.toISOString() 77 | dateStr = dateStr.split('.')[0].replace('T', ' ') 78 | dateStr = dateStr.slice(0, dateStr.lastIndexOf(':')) + 'Z' 79 | 80 | htmlList.push( 81 | ` ` + 82 | `${name}` + 83 | `${dateStr}${niceBytes(file.size)}`); 84 | 85 | if (lastModified == null || file.uploaded > lastModified) { 86 | lastModified = file.uploaded; 87 | } 88 | 89 | } 90 | 91 | if (path === "") path = "/"; 92 | 93 | html = ` 94 | 95 | 96 | Index of ${path} 97 | 98 | 99 | 117 | 118 | 119 |

Index of ${path}

120 | 121 | 122 | ${htmlList.join("\n")} 123 |
FilenameModifiedSize
124 | 125 | 126 | ` 127 | }; 128 | 129 | return new Response(html === "" ? null : html, { 130 | status: 200, 131 | headers: { 132 | "access-control-allow-origin": env.ALLOWED_ORIGINS || "", 133 | "last-modified": lastModified === null ? "" : lastModified.toUTCString(), 134 | "content-type": "text/html", 135 | "cache-control": env.DIRECTORY_CACHE_CONTROL || "no-store" 136 | } 137 | }); 138 | } 139 | 140 | export default { 141 | async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise { 142 | const allowedMethods = ["GET", "HEAD", "OPTIONS"]; 143 | if (allowedMethods.indexOf(request.method) === -1) return new Response("Method Not Allowed", { status: 405 }); 144 | 145 | if (request.method === "OPTIONS") { 146 | return new Response(null, { headers: { "allow": allowedMethods.join(", ") } }) 147 | } 148 | 149 | let triedIndex = false; 150 | 151 | const url = new URL(request.url); 152 | let response: Response | undefined; 153 | 154 | const isCachingEnabled = env.CACHE_CONTROL !== "no-store" 155 | const cache = caches.default; 156 | if (isCachingEnabled) { 157 | response = await cache.match(request); 158 | } 159 | 160 | // Since we produce this result from the request, we don't need to strictly use an R2Range 161 | let range: ParsedRange | undefined; 162 | 163 | if (!response || !(response.ok || response.status == 304)) { 164 | console.warn("Cache miss"); 165 | let path = (env.PATH_PREFIX || "") + decodeURIComponent(url.pathname); 166 | 167 | // directory logic 168 | if (path.endsWith("/")) { 169 | // if theres an index file, try that. 404 logic down below has dir fallback. 170 | if (env.INDEX_FILE && env.INDEX_FILE !== "") { 171 | path += env.INDEX_FILE; 172 | triedIndex = true; 173 | } else if (env.DIRECTORY_LISTING) { 174 | // return the dir listing 175 | let listResponse = await makeListingResponse(path, env, request); 176 | 177 | if (listResponse !== null) { 178 | if (listResponse.headers.get("cache-control") !== "no-store") { 179 | ctx.waitUntil(cache.put(request, listResponse.clone())); 180 | } 181 | return listResponse; 182 | } 183 | } 184 | } 185 | 186 | if (path !== "/" && path.startsWith("/")) { 187 | path = path.substring(1); 188 | } 189 | 190 | let file: R2Object | R2ObjectBody | null | undefined; 191 | 192 | // Range handling 193 | if (request.method === "GET") { 194 | const rangeHeader = request.headers.get("range"); 195 | if (rangeHeader) { 196 | file = await env.R2_BUCKET.head(path); 197 | if (file === null) return new Response("File Not Found", { status: 404 }); 198 | const parsedRanges = parseRange(file.size, rangeHeader); 199 | // R2 only supports 1 range at the moment, reject if there is more than one 200 | if (parsedRanges !== -1 && parsedRanges !== -2 && parsedRanges.length === 1 && parsedRanges.type === "bytes") { 201 | let firstRange = parsedRanges[0]; 202 | range = file.size === (firstRange.end + 1) ? { suffix: file.size - firstRange.start } : { 203 | offset: firstRange.start, 204 | length: firstRange.end - firstRange.start + 1 205 | }; 206 | } else { 207 | return new Response("Range Not Satisfiable", { status: 416 }); 208 | } 209 | } 210 | } 211 | 212 | // Etag/If-(Not)-Match handling 213 | // R2 requires that etag checks must not contain quotes, and the S3 spec only allows one etag 214 | // This silently ignores invalid or weak (W/) headers 215 | const getHeaderEtag = (header: string | null) => header?.trim().replace(/^['"]|['"]$/g, ""); 216 | const ifMatch = getHeaderEtag(request.headers.get("if-match")); 217 | const ifNoneMatch = getHeaderEtag(request.headers.get("if-none-match")); 218 | 219 | const ifModifiedSince = Date.parse(request.headers.get("if-modified-since") || ""); 220 | const ifUnmodifiedSince = Date.parse(request.headers.get("if-unmodified-since") || ""); 221 | 222 | const ifRange = request.headers.get("if-range"); 223 | if (range && ifRange && file) { 224 | const maybeDate = Date.parse(ifRange); 225 | 226 | if (isNaN(maybeDate) || new Date(maybeDate) > file.uploaded) { 227 | // httpEtag already has quotes, no need to use getHeaderEtag 228 | if (ifRange.startsWith("W/") || ifRange !== file.httpEtag) range = undefined; 229 | } 230 | } 231 | 232 | if (ifMatch || ifUnmodifiedSince) { 233 | file = await env.R2_BUCKET.get(path, { 234 | onlyIf: { 235 | etagMatches: ifMatch, 236 | uploadedBefore: ifUnmodifiedSince ? new Date(ifUnmodifiedSince) : undefined 237 | }, range 238 | }); 239 | 240 | if (file && !hasBody(file)) { 241 | return new Response("Precondition Failed", { status: 412 }); 242 | } 243 | } 244 | 245 | if (ifNoneMatch || ifModifiedSince) { 246 | // if-none-match overrides if-modified-since completely 247 | if (ifNoneMatch) { 248 | file = await env.R2_BUCKET.get(path, { onlyIf: { etagDoesNotMatch: ifNoneMatch }, range }); 249 | } else if (ifModifiedSince) { 250 | file = await env.R2_BUCKET.get(path, { onlyIf: { uploadedAfter: new Date(ifModifiedSince) }, range }); 251 | } 252 | if (file && !hasBody(file)) { 253 | return new Response(null, { status: 304 }); 254 | } 255 | } 256 | 257 | file = request.method === "HEAD" 258 | ? await env.R2_BUCKET.head(path) 259 | : ((file && hasBody(file)) ? file : await env.R2_BUCKET.get(path, { range })); 260 | 261 | let notFound: boolean = false; 262 | 263 | if (file === null) { 264 | if (env.INDEX_FILE && triedIndex) { 265 | // remove the index file since it doesnt exist 266 | path = path.substring(0, path.length - env.INDEX_FILE.length) 267 | } 268 | 269 | if (env.DIRECTORY_LISTING && (path.endsWith("/") || path === "")) { 270 | // return the dir listing 271 | let listResponse = await makeListingResponse(path, env, request); 272 | 273 | if (listResponse !== null) { 274 | if (listResponse.headers.get("cache-control") !== "no-store") { 275 | ctx.waitUntil(cache.put(request, listResponse.clone())); 276 | } 277 | return listResponse; 278 | } 279 | } 280 | 281 | if (env.NOTFOUND_FILE && env.NOTFOUND_FILE != "") { 282 | notFound = true; 283 | path = env.NOTFOUND_FILE; 284 | file = request.method === "HEAD" 285 | ? await env.R2_BUCKET.head(path) 286 | : await env.R2_BUCKET.get(path); 287 | } 288 | 289 | // if its still null, either 404 is disabled or that file wasn't found either 290 | // this isn't an else because then there would have to be two of theem 291 | if (file == null) { 292 | return new Response("File Not Found", { status: 404 }); 293 | } 294 | } 295 | 296 | response = new Response((hasBody(file) && file.size !== 0) ? file.body : null, { 297 | status: notFound ? 404 : (range ? 206 : 200), 298 | headers: { 299 | "accept-ranges": "bytes", 300 | "access-control-allow-origin": env.ALLOWED_ORIGINS || "", 301 | 302 | "etag": notFound ? "" : file.httpEtag, 303 | // if the 404 file has a custom cache control, we respect it 304 | "cache-control": file.httpMetadata?.cacheControl ?? (notFound ? "" : env.CACHE_CONTROL || ""), 305 | "expires": file.httpMetadata?.cacheExpiry?.toUTCString() ?? "", 306 | "last-modified": notFound ? "" : file.uploaded.toUTCString(), 307 | 308 | "content-encoding": file.httpMetadata?.contentEncoding ?? "", 309 | "content-type": file.httpMetadata?.contentType ?? "application/octet-stream", 310 | "content-language": file.httpMetadata?.contentLanguage ?? "", 311 | "content-disposition": file.httpMetadata?.contentDisposition ?? "", 312 | "content-range": (range && !notFound ? getRangeHeader(range, file.size) : ""), 313 | "content-length": (range && !notFound ? (rangeHasLength(range) ? range.length : range.suffix) : file.size).toString() 314 | } 315 | }); 316 | 317 | if (request.method === "GET" && !range && isCachingEnabled && !notFound) 318 | ctx.waitUntil(cache.put(request, response.clone())); 319 | } 320 | 321 | return response; 322 | }, 323 | }; 324 | 325 | function niceBytes(x: number) { 326 | 327 | let l = 0, n = parseInt(x.toString(), 10) || 0; 328 | 329 | while (n >= 1000 && ++l) { 330 | n = n / 1000; 331 | } 332 | 333 | return (n.toFixed(n < 10 && l > 0 ? 1 : 0) + ' ' + units[l]); 334 | } 335 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | /* Projects */ 5 | // "incremental": true, /* Enable incremental compilation */ 6 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 7 | // "tsBuildInfoFile": "./", /* Specify the folder for .tsbuildinfo incremental compilation files. */ 8 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects */ 9 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 10 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 11 | /* Language and Environment */ 12 | "target": "es2021" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, 13 | "lib": [ 14 | "es2021" 15 | ] /* Specify a set of bundled library declaration files that describe the target runtime environment. */, 16 | // "jsx": "preserve", /* Specify what JSX code is generated. */ 17 | // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ 18 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 19 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h' */ 20 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 21 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using `jsx: react-jsx*`.` */ 22 | // "reactNamespace": "", /* Specify the object invoked for `createElement`. This only applies when targeting `react` JSX emit. */ 23 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 24 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 25 | /* Modules */ 26 | "module": "es2022" /* Specify what module code is generated. */, 27 | "rootDir": "./src" /* Specify the root folder within your source files. */, 28 | "moduleResolution": "node" /* Specify how TypeScript looks up a file from a given module specifier. */, 29 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 30 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 31 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 32 | // "typeRoots": [], /* Specify multiple folders that act like `./node_modules/@types`. */ 33 | "types": [ 34 | "@cloudflare/workers-types" 35 | ] /* Specify type package names to be included without being referenced in a source file. */, 36 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 37 | "resolveJsonModule": true /* Enable importing .json files */, 38 | // "noResolve": true, /* Disallow `import`s, `require`s or ``s from expanding the number of files TypeScript should add to a project. */ 39 | /* JavaScript Support */ 40 | "allowJs": true /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */, 41 | "checkJs": true /* Enable error reporting in type-checked JavaScript files. */, 42 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from `node_modules`. Only applicable with `allowJs`. */ 43 | /* Emit */ 44 | "declaration": true /* Generate .d.ts files from TypeScript and JavaScript files in your project. */, 45 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 46 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 47 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 48 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If `declaration` is true, also designates a file that bundles all .d.ts output. */ 49 | "outDir": "./dist" /* Specify an output folder for all emitted files. */, 50 | // "removeComments": true, /* Disable emitting comments. */ 51 | // "noEmit": true /* Disable emitting files from a compilation. */, 52 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 53 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types */ 54 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 55 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 56 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 57 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 58 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 59 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 60 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 61 | // "stripInternal": true, /* Disable emitting declarations that have `@internal` in their JSDoc comments. */ 62 | // "noEmitHelpers": true, /* Disable generating custom helper functions like `__extends` in compiled output. */ 63 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 64 | // "preserveConstEnums": true, /* Disable erasing `const enum` declarations in generated code. */ 65 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 66 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ 67 | /* Interop Constraints */ 68 | "isolatedModules": true /* Ensure that each file can be safely transpiled without relying on other imports. */, 69 | "allowSyntheticDefaultImports": true /* Allow 'import x from y' when a module doesn't have a default export. */, 70 | // "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */, 71 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 72 | "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, 73 | /* Type Checking */ 74 | "strict": true /* Enable all strict type-checking options. */, 75 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied `any` type.. */ 76 | // "strictNullChecks": true, /* When type checking, take into account `null` and `undefined`. */ 77 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 78 | // "strictBindCallApply": true, /* Check that the arguments for `bind`, `call`, and `apply` methods match the original function. */ 79 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 80 | // "noImplicitThis": true, /* Enable error reporting when `this` is given the type `any`. */ 81 | // "useUnknownInCatchVariables": true, /* Type catch clause variables as 'unknown' instead of 'any'. */ 82 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 83 | // "noUnusedLocals": true, /* Enable error reporting when a local variables aren't read. */ 84 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read */ 85 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 86 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 87 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 88 | // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ 89 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 90 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type */ 91 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 92 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 93 | /* Completeness */ 94 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 95 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /wrangler.toml: -------------------------------------------------------------------------------- 1 | name = "render" 2 | main = "src/index.ts" 3 | compatibility_date = "2022-05-15" 4 | # Set this to false if you don't want to use the default *.workers.dev route. 5 | # Note that *.workers.dev routes don't support native worker-level caching: https://developers.cloudflare.com/workers/runtime-apis/cache/ 6 | workers_dev = true 7 | 8 | [vars] 9 | # The `access-control-allow-origin` header: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin 10 | # Optional, the `access-control-allow-origin` header is omitted if unset, which blocks all cross-origin requests. 11 | ALLOWED_ORIGINS = "" 12 | 13 | # The `cache-control` header: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control. 14 | # Optional, the `cache-control` header is omitted if unset, which would NOT disable caching: https://developers.cloudflare.com/workers/runtime-apis/cache/#headers 15 | # For example, you can disable all cache by setting this to `no-store`. 16 | CACHE_CONTROL = "max-age=86400" 17 | 18 | # The string to prepend to each file path. Optional, nothing is prepended to the path if unset. 19 | PATH_PREFIX = "" 20 | 21 | # Index file to search for on directory requests, set to "" to disable indexes 22 | # Relative to the directory of the request. 23 | #INDEX_FILE = "" 24 | INDEX_FILE = "index.html" 25 | 26 | # File to fall back to when the requested path is not found in the bucket. 27 | # Incurs an additional read operation for 404 requests. 28 | # Set to "" to disable custom 404 fallbacks. 29 | # Relative to the root of the bucket. 30 | NOTFOUND_FILE = "" 31 | #NOT_FOUND_FILE = "404.html" 32 | 33 | # Enable to show a directory listing fallback on paths ending in / 34 | # If INDEX_FILE is also provided, it will be used instead if the file exists. 35 | DIRECTORY_LISTING = false 36 | 37 | # Enable to hide files or directories beginning with . from directory listings. 38 | HIDE_HIDDEN_FILES = false 39 | 40 | # Set a cache header here, e.g. "max-age=86400", if you want to cache directory listings. 41 | DIRECTORY_CACHE_CONTROL = "no-store" 42 | 43 | [[r2_buckets]] 44 | binding = "R2_BUCKET" 45 | bucket_name = "kot" # Set this to your R2 bucket name. Required 46 | preview_bucket_name = "kot" # Set this to your preview R2 bucket name. Can be equal to bucket_name. Optional 47 | --------------------------------------------------------------------------------