├── .gitattributes ├── .github ├── dependabot.yml └── workflows │ └── workers.yml ├── .gitignore ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── biome.json ├── package.json ├── packages └── analytics │ ├── package.json │ ├── src │ └── index.ts │ └── tsconfig.json ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── tsconfig.json ├── vitest.workspace.js └── workers ├── apigw ├── README.md ├── package.json ├── src │ └── index.ts ├── test │ ├── env.d.ts │ └── index.spec.ts ├── tsconfig.json ├── vitest.config.js └── wrangler.toml ├── echo ├── README.md ├── package.json ├── src │ └── index.ts ├── test │ └── index.spec.ts ├── tsconfig.json ├── vitest.config.js └── wrangler.toml ├── hash ├── README.md ├── package.json ├── src │ └── index.ts ├── test │ └── index.spec.ts ├── tsconfig.json ├── vitest.config.js └── wrangler.toml ├── hello-world ├── package.json ├── src │ └── index.ts ├── test │ └── index.spec.ts ├── tsconfig.json ├── vitest.config.js └── wrangler.toml ├── ip ├── README.md ├── package.json ├── src │ └── index.ts ├── test │ └── index.spec.ts ├── tsconfig.json ├── vitest.config.js └── wrangler.toml ├── paste ├── README.md ├── migrations │ └── 001_schema.sql ├── package.json ├── src │ └── index.ts ├── test │ ├── apply-migrations.ts │ ├── env.d.ts │ └── index.spec.ts ├── tsconfig.json ├── vitest.config.js └── wrangler.toml ├── rand ├── README.md ├── package.json ├── src │ └── index.ts ├── test │ └── index.spec.ts ├── tsconfig.json ├── vitest.config.js └── wrangler.toml ├── rdap ├── README.md ├── migrations │ └── 001_schema.sql ├── package.json ├── src │ └── index.ts ├── test │ └── index.spec.ts ├── tsconfig.json ├── vitest.config.js └── wrangler.toml ├── stocks ├── README.md ├── package.json ├── src │ ├── common │ │ └── quote.ts │ ├── index.ts │ └── yahoofinance │ │ └── index.ts ├── test │ └── index.spec.ts ├── tsconfig.json ├── vitest.config.js └── wrangler.toml └── tfstate ├── README.md ├── example ├── .terraform.lock.hcl ├── errored.tfstate └── main.tf ├── package.json ├── src ├── durableLock.ts ├── handlers.ts ├── index.ts ├── middlewares.ts ├── sha256.ts ├── types │ ├── env.d.ts │ ├── request.d.ts │ └── terraform.d.ts └── utils.ts ├── test ├── env.d.ts └── index.spec.ts ├── tsconfig.json ├── vitest.config.js └── wrangler.toml /.gitattributes: -------------------------------------------------------------------------------- 1 | #exclude lockfiles from git diff 2 | package-lock.json -diff 3 | pnpm-lock.yaml -diff 4 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: npm 9 | directory: "/" 10 | schedule: 11 | interval: "weekly" 12 | groups: 13 | production-dependencies: 14 | dependency-type: "production" 15 | dev-dependencies: 16 | dependency-type: "development" 17 | 18 | - package-ecosystem: "github-actions" 19 | directory: ".github/workflows" 20 | schedule: 21 | interval: "weekly" 22 | -------------------------------------------------------------------------------- /.github/workflows/workers.yml: -------------------------------------------------------------------------------- 1 | name: Workers 2 | 3 | on: 4 | push: 5 | branches: ["main"] 6 | pull_request: 7 | branches: ["main"] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | - uses: pnpm/action-setup@v4 15 | with: 16 | version: 9 17 | - uses: actions/setup-node@v4 18 | with: 19 | node-version: lts/* 20 | - run: pnpm install 21 | - name: Setup Biome 22 | uses: biomejs/setup-biome@v2 23 | - name: Run Biome 24 | run: biome ci . 25 | - run: pnpm run test 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .idea/ 3 | dist/ 4 | .terraform/ 5 | # W.I.P 6 | packages/auth 7 | secrets.auto.tfvars 8 | pages/blog/public 9 | pkg/ 10 | .wrangler/ 11 | .DS_Store 12 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "[javascript]": { 3 | "editor.defaultFormatter": "biomejs.biome" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Cole Mackenzie 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 | # Holster 2 | 3 | A collection of Cloudflare Workers, managed using PNPM Workspaces. 4 | 5 | ## Apps 6 | 7 | | Name | Host | Description | 8 | | -------------------------------------- | ----------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ | 9 | | [echo](./workers/echo/README.md) | [`echo.mirio.dev`](https://echo.mirio.dev) | Echos back the received HTTP request. | 10 | | [hash](./workers/hash/README.md) | [`hash.mirio.dev`](https://hash.mirio.dev) | Returns the hex digest of the payload. Supports `md5`, `sha1`, `sha256` `sha384` and `sha512` | 11 | | [ip](./workers/ip/README.md) | [`ip.mirio.dev`](https://ip.mirio.dev) | Information about the clients IP address. | 12 | | [paste](./workers/paste/README.md) | [`paste.mirio.dev`](https://paste.mirio.dev) | Share small snippets of code using the web! All Pastes expire after 7 days. | 13 | | [rand](./workers/rand/README.md) | [`rand.mirio.dev`](https://rand.mirio.dev) | Get random values, like UUID's! | 14 | | [rdap](./workers/rdap/README.md) | [`rdap.mirio.dev`](https://rdap.mirio.dev) | Query RDAP (Registration Data Access Protocol) servers for domain registration information. | 15 | | [stocks](./workers/stocks/README.md) | [`stocks.mirio.dev`](https://stocks.mirio.dev) | Get quick information about your favorite stock symbols! | 16 | | [tfstate](./workers/tfstate/README.md) | `tfstate.mirio.dev`
`apigw.eragon.xyz/tfstate/` | Manage terraform state using a HTTP backend. Use the `apigw` route to proxy basic auth to CF Access headers. | 17 | 18 | ## Create a new Worker 19 | 20 | ```bash 21 | npm create cloudflare@latest 22 | ``` 23 | -------------------------------------------------------------------------------- /biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://biomejs.dev/schemas/1.6.4/schema.json", 3 | "files": { 4 | "ignore": ["node_modules", ".wrangler", "dist"] 5 | }, 6 | "organizeImports": { 7 | "enabled": true 8 | }, 9 | "linter": { 10 | "enabled": true, 11 | "rules": { 12 | "recommended": true, 13 | "correctness": { 14 | "noUnusedImports": { 15 | "level": "warn" 16 | } 17 | } 18 | } 19 | }, 20 | "formatter": { 21 | "enabled": true 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "root", 3 | "private": true, 4 | "scripts": { 5 | "lint": "biome lint --write ./ && tsc --noEmit", 6 | "format": "biome format --write ./", 7 | "test": "vitest" 8 | }, 9 | "devDependencies": { 10 | "@cloudflare/workers-types": "catalog:", 11 | "@cloudflare/vitest-pool-workers": "catalog:", 12 | "@biomejs/biome": "1.9.4", 13 | "typescript": "^5.8.2", 14 | "vitest": "catalog:" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /packages/analytics/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@mirio/analytics", 3 | "version": "1.0.0", 4 | "private": true, 5 | "description": "", 6 | "main": "./src/index.ts", 7 | "scripts": { 8 | "test": "vitest" 9 | }, 10 | "devDependencies": { 11 | "wrangler": "catalog:", 12 | "@cloudflare/vitest-pool-workers": "catalog:", 13 | "@cloudflare/workers-types": "catalog:", 14 | "vitest": "catalog:" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /packages/analytics/src/index.ts: -------------------------------------------------------------------------------- 1 | export class HTTPAnalytics { 2 | binding?: AnalyticsEngineDataset; 3 | 4 | constructor(binding?: AnalyticsEngineDataset) { 5 | this.binding = binding; 6 | } 7 | 8 | public observe(request: Request): void { 9 | if (!this.binding) { 10 | return; 11 | } 12 | 13 | const url = new URL(request.url); 14 | const { method, cf } = request; 15 | const ip = request.headers.get("cf-connecting-ip"); 16 | 17 | this.binding.writeDataPoint({ 18 | indexes: [url.hostname], 19 | blobs: [ 20 | method, // HTTP method 21 | request.url, // URL 22 | ip, // IP address, can be IPv4 or IPv6 23 | (cf?.httpProtocol as string) || "unknown", // HTTP protocol 24 | (cf?.colo as string) || "unknown", // Cloudflare data center 25 | (cf?.latitude as string) || "unknown", // Latitude 26 | (cf?.longitude as string) || "unknown", // Longitude 27 | ], 28 | }); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/analytics/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json" 3 | } 4 | -------------------------------------------------------------------------------- /pnpm-lock.yaml: -------------------------------------------------------------------------------- 1 | lockfileVersion: '9.0' 2 | 3 | settings: 4 | autoInstallPeers: true 5 | excludeLinksFromLockfile: false 6 | 7 | catalogs: 8 | default: 9 | '@cloudflare/vitest-pool-workers': 10 | specifier: ^0.8.0 11 | version: 0.8.8 12 | '@cloudflare/workers-types': 13 | specifier: ^4.20250313.0 14 | version: 4.20250327.0 15 | vitest: 16 | specifier: 3.0.8 17 | version: 3.0.8 18 | wrangler: 19 | specifier: ^4.6.0 20 | version: 4.6.0 21 | 22 | importers: 23 | 24 | .: 25 | devDependencies: 26 | '@biomejs/biome': 27 | specifier: 1.9.4 28 | version: 1.9.4 29 | '@cloudflare/vitest-pool-workers': 30 | specifier: 'catalog:' 31 | version: 0.8.8(@cloudflare/workers-types@4.20250327.0)(@vitest/runner@3.0.9)(@vitest/snapshot@3.0.9)(vitest@3.0.8(@types/node@20.14.2)) 32 | '@cloudflare/workers-types': 33 | specifier: 'catalog:' 34 | version: 4.20250327.0 35 | typescript: 36 | specifier: ^5.8.2 37 | version: 5.8.2 38 | vitest: 39 | specifier: 'catalog:' 40 | version: 3.0.8(@types/node@20.14.2) 41 | 42 | packages/analytics: 43 | devDependencies: 44 | '@cloudflare/vitest-pool-workers': 45 | specifier: 'catalog:' 46 | version: 0.8.8(@cloudflare/workers-types@4.20250327.0)(@vitest/runner@3.0.9)(@vitest/snapshot@3.0.9)(vitest@3.0.8(@types/node@20.14.2)) 47 | '@cloudflare/workers-types': 48 | specifier: 'catalog:' 49 | version: 4.20250327.0 50 | vitest: 51 | specifier: 'catalog:' 52 | version: 3.0.8(@types/node@20.14.2) 53 | wrangler: 54 | specifier: 'catalog:' 55 | version: 4.6.0(@cloudflare/workers-types@4.20250327.0) 56 | 57 | workers/apigw: 58 | dependencies: 59 | itty-router: 60 | specifier: ^5.0.18 61 | version: 5.0.18 62 | devDependencies: 63 | '@cloudflare/vitest-pool-workers': 64 | specifier: 'catalog:' 65 | version: 0.8.8(@cloudflare/workers-types@4.20250327.0)(@vitest/runner@3.0.9)(@vitest/snapshot@3.0.9)(vitest@3.0.8(@types/node@20.14.2)) 66 | '@cloudflare/workers-types': 67 | specifier: 'catalog:' 68 | version: 4.20250327.0 69 | vitest: 70 | specifier: 'catalog:' 71 | version: 3.0.8(@types/node@20.14.2) 72 | wrangler: 73 | specifier: 'catalog:' 74 | version: 4.6.0(@cloudflare/workers-types@4.20250327.0) 75 | 76 | workers/echo: 77 | devDependencies: 78 | '@cloudflare/vitest-pool-workers': 79 | specifier: 'catalog:' 80 | version: 0.8.8(@cloudflare/workers-types@4.20250327.0)(@vitest/runner@3.0.9)(@vitest/snapshot@3.0.9)(vitest@3.0.8(@types/node@20.14.2)) 81 | '@cloudflare/workers-types': 82 | specifier: 'catalog:' 83 | version: 4.20250327.0 84 | vitest: 85 | specifier: 'catalog:' 86 | version: 3.0.8(@types/node@20.14.2) 87 | wrangler: 88 | specifier: 'catalog:' 89 | version: 4.6.0(@cloudflare/workers-types@4.20250327.0) 90 | 91 | workers/hash: 92 | dependencies: 93 | itty-router: 94 | specifier: ^5.0.18 95 | version: 5.0.18 96 | devDependencies: 97 | '@cloudflare/vitest-pool-workers': 98 | specifier: 'catalog:' 99 | version: 0.8.8(@cloudflare/workers-types@4.20250327.0)(@vitest/runner@3.0.9)(@vitest/snapshot@3.0.9)(vitest@3.0.8(@types/node@20.14.2)) 100 | '@cloudflare/workers-types': 101 | specifier: 'catalog:' 102 | version: 4.20250327.0 103 | vitest: 104 | specifier: 'catalog:' 105 | version: 3.0.8(@types/node@20.14.2) 106 | wrangler: 107 | specifier: 'catalog:' 108 | version: 4.6.0(@cloudflare/workers-types@4.20250327.0) 109 | 110 | workers/hello-world: 111 | dependencies: 112 | '@mirio/analytics': 113 | specifier: 'workspace:' 114 | version: link:../../packages/analytics 115 | devDependencies: 116 | '@cloudflare/vitest-pool-workers': 117 | specifier: 'catalog:' 118 | version: 0.8.8(@cloudflare/workers-types@4.20250327.0)(@vitest/runner@3.0.9)(@vitest/snapshot@3.0.9)(vitest@3.0.8(@types/node@20.14.2)) 119 | '@cloudflare/workers-types': 120 | specifier: 'catalog:' 121 | version: 4.20250327.0 122 | vitest: 123 | specifier: 'catalog:' 124 | version: 3.0.8(@types/node@20.14.2) 125 | wrangler: 126 | specifier: 'catalog:' 127 | version: 4.6.0(@cloudflare/workers-types@4.20250327.0) 128 | 129 | workers/ip: 130 | dependencies: 131 | itty-router: 132 | specifier: ^5.0.18 133 | version: 5.0.18 134 | devDependencies: 135 | '@cloudflare/vitest-pool-workers': 136 | specifier: 'catalog:' 137 | version: 0.8.8(@cloudflare/workers-types@4.20250327.0)(@vitest/runner@3.0.9)(@vitest/snapshot@3.0.9)(vitest@3.0.8(@types/node@20.14.2)) 138 | '@cloudflare/workers-types': 139 | specifier: 'catalog:' 140 | version: 4.20250327.0 141 | vitest: 142 | specifier: 'catalog:' 143 | version: 3.0.8(@types/node@20.14.2) 144 | wrangler: 145 | specifier: 'catalog:' 146 | version: 4.6.0(@cloudflare/workers-types@4.20250327.0) 147 | 148 | workers/paste: 149 | dependencies: 150 | hono: 151 | specifier: ^4.7.5 152 | version: 4.7.5 153 | short-unique-id: 154 | specifier: ^5.2.2 155 | version: 5.2.2 156 | devDependencies: 157 | '@cloudflare/vitest-pool-workers': 158 | specifier: 'catalog:' 159 | version: 0.8.8(@cloudflare/workers-types@4.20250327.0)(@vitest/runner@3.0.9)(@vitest/snapshot@3.0.9)(vitest@3.0.8(@types/node@20.14.2)) 160 | '@cloudflare/workers-types': 161 | specifier: 'catalog:' 162 | version: 4.20250327.0 163 | vitest: 164 | specifier: 'catalog:' 165 | version: 3.0.8(@types/node@20.14.2) 166 | wrangler: 167 | specifier: 'catalog:' 168 | version: 4.6.0(@cloudflare/workers-types@4.20250327.0) 169 | 170 | workers/rand: 171 | dependencies: 172 | itty-router: 173 | specifier: ^5.0.18 174 | version: 5.0.18 175 | devDependencies: 176 | '@cloudflare/vitest-pool-workers': 177 | specifier: 'catalog:' 178 | version: 0.8.8(@cloudflare/workers-types@4.20250327.0)(@vitest/runner@3.0.9)(@vitest/snapshot@3.0.9)(vitest@3.0.8(@types/node@20.14.2)) 179 | '@cloudflare/workers-types': 180 | specifier: 'catalog:' 181 | version: 4.20250327.0 182 | vitest: 183 | specifier: 'catalog:' 184 | version: 3.0.8(@types/node@20.14.2) 185 | wrangler: 186 | specifier: 'catalog:' 187 | version: 4.6.0(@cloudflare/workers-types@4.20250327.0) 188 | 189 | workers/rdap: 190 | dependencies: 191 | hono: 192 | specifier: ^4.7.5 193 | version: 4.7.5 194 | devDependencies: 195 | '@cloudflare/vitest-pool-workers': 196 | specifier: 'catalog:' 197 | version: 0.8.8(@cloudflare/workers-types@4.20250327.0)(@vitest/runner@3.0.9)(@vitest/snapshot@3.0.9)(vitest@3.0.8(@types/node@20.14.2)) 198 | '@cloudflare/workers-types': 199 | specifier: 'catalog:' 200 | version: 4.20250327.0 201 | vitest: 202 | specifier: 'catalog:' 203 | version: 3.0.8(@types/node@20.14.2) 204 | wrangler: 205 | specifier: 'catalog:' 206 | version: 4.6.0(@cloudflare/workers-types@4.20250327.0) 207 | 208 | workers/stocks: 209 | dependencies: 210 | itty-router: 211 | specifier: ^5.0.18 212 | version: 5.0.18 213 | devDependencies: 214 | '@cloudflare/vitest-pool-workers': 215 | specifier: 'catalog:' 216 | version: 0.8.8(@cloudflare/workers-types@4.20250327.0)(@vitest/runner@3.0.9)(@vitest/snapshot@3.0.9)(vitest@3.0.8(@types/node@20.14.2)) 217 | '@cloudflare/workers-types': 218 | specifier: 'catalog:' 219 | version: 4.20250327.0 220 | vitest: 221 | specifier: 'catalog:' 222 | version: 3.0.8(@types/node@20.14.2) 223 | wrangler: 224 | specifier: 'catalog:' 225 | version: 4.6.0(@cloudflare/workers-types@4.20250327.0) 226 | 227 | workers/tfstate: 228 | dependencies: 229 | itty-router: 230 | specifier: ^5.0.18 231 | version: 5.0.18 232 | jose: 233 | specifier: ^6.0.10 234 | version: 6.0.10 235 | devDependencies: 236 | '@cloudflare/vitest-pool-workers': 237 | specifier: 'catalog:' 238 | version: 0.8.8(@cloudflare/workers-types@4.20250327.0)(@vitest/runner@3.0.9)(@vitest/snapshot@3.0.9)(vitest@3.0.8(@types/node@20.14.2)) 239 | '@cloudflare/workers-types': 240 | specifier: 'catalog:' 241 | version: 4.20250327.0 242 | vitest: 243 | specifier: 'catalog:' 244 | version: 3.0.8(@types/node@20.14.2) 245 | wrangler: 246 | specifier: 'catalog:' 247 | version: 4.6.0(@cloudflare/workers-types@4.20250327.0) 248 | 249 | packages: 250 | 251 | '@biomejs/biome@1.9.4': 252 | resolution: {integrity: sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog==} 253 | engines: {node: '>=14.21.3'} 254 | hasBin: true 255 | 256 | '@biomejs/cli-darwin-arm64@1.9.4': 257 | resolution: {integrity: sha512-bFBsPWrNvkdKrNCYeAp+xo2HecOGPAy9WyNyB/jKnnedgzl4W4Hb9ZMzYNbf8dMCGmUdSavlYHiR01QaYR58cw==} 258 | engines: {node: '>=14.21.3'} 259 | cpu: [arm64] 260 | os: [darwin] 261 | 262 | '@biomejs/cli-darwin-x64@1.9.4': 263 | resolution: {integrity: sha512-ngYBh/+bEedqkSevPVhLP4QfVPCpb+4BBe2p7Xs32dBgs7rh9nY2AIYUL6BgLw1JVXV8GlpKmb/hNiuIxfPfZg==} 264 | engines: {node: '>=14.21.3'} 265 | cpu: [x64] 266 | os: [darwin] 267 | 268 | '@biomejs/cli-linux-arm64-musl@1.9.4': 269 | resolution: {integrity: sha512-v665Ct9WCRjGa8+kTr0CzApU0+XXtRgwmzIf1SeKSGAv+2scAlW6JR5PMFo6FzqqZ64Po79cKODKf3/AAmECqA==} 270 | engines: {node: '>=14.21.3'} 271 | cpu: [arm64] 272 | os: [linux] 273 | 274 | '@biomejs/cli-linux-arm64@1.9.4': 275 | resolution: {integrity: sha512-fJIW0+LYujdjUgJJuwesP4EjIBl/N/TcOX3IvIHJQNsAqvV2CHIogsmA94BPG6jZATS4Hi+xv4SkBBQSt1N4/g==} 276 | engines: {node: '>=14.21.3'} 277 | cpu: [arm64] 278 | os: [linux] 279 | 280 | '@biomejs/cli-linux-x64-musl@1.9.4': 281 | resolution: {integrity: sha512-gEhi/jSBhZ2m6wjV530Yy8+fNqG8PAinM3oV7CyO+6c3CEh16Eizm21uHVsyVBEB6RIM8JHIl6AGYCv6Q6Q9Tg==} 282 | engines: {node: '>=14.21.3'} 283 | cpu: [x64] 284 | os: [linux] 285 | 286 | '@biomejs/cli-linux-x64@1.9.4': 287 | resolution: {integrity: sha512-lRCJv/Vi3Vlwmbd6K+oQ0KhLHMAysN8lXoCI7XeHlxaajk06u7G+UsFSO01NAs5iYuWKmVZjmiOzJ0OJmGsMwg==} 288 | engines: {node: '>=14.21.3'} 289 | cpu: [x64] 290 | os: [linux] 291 | 292 | '@biomejs/cli-win32-arm64@1.9.4': 293 | resolution: {integrity: sha512-tlbhLk+WXZmgwoIKwHIHEBZUwxml7bRJgk0X2sPyNR3S93cdRq6XulAZRQJ17FYGGzWne0fgrXBKpl7l4M87Hg==} 294 | engines: {node: '>=14.21.3'} 295 | cpu: [arm64] 296 | os: [win32] 297 | 298 | '@biomejs/cli-win32-x64@1.9.4': 299 | resolution: {integrity: sha512-8Y5wMhVIPaWe6jw2H+KlEm4wP/f7EW3810ZLmDlrEEy5KvBsb9ECEfu/kMWD484ijfQ8+nIi0giMgu9g1UAuuA==} 300 | engines: {node: '>=14.21.3'} 301 | cpu: [x64] 302 | os: [win32] 303 | 304 | '@cloudflare/kv-asset-handler@0.4.0': 305 | resolution: {integrity: sha512-+tv3z+SPp+gqTIcImN9o0hqE9xyfQjI1XD9pL6NuKjua9B1y7mNYv0S9cP+QEbA4ppVgGZEmKOvHX5G5Ei1CVA==} 306 | engines: {node: '>=18.0.0'} 307 | 308 | '@cloudflare/unenv-preset@2.3.1': 309 | resolution: {integrity: sha512-Xq57Qd+ADpt6hibcVBO0uLG9zzRgyRhfCUgBT9s+g3+3Ivg5zDyVgLFy40ES1VdNcu8rPNSivm9A+kGP5IVaPg==} 310 | peerDependencies: 311 | unenv: 2.0.0-rc.15 312 | workerd: ^1.20250320.0 313 | peerDependenciesMeta: 314 | workerd: 315 | optional: true 316 | 317 | '@cloudflare/vitest-pool-workers@0.8.8': 318 | resolution: {integrity: sha512-DrAZogDljs2QfX6JffzviOxI8BT9lhoFJiKs44rGHXiErgaLlr+eqUup63tJ89JBiM2wjHvBvIRF4hvVY3o6xQ==} 319 | peerDependencies: 320 | '@vitest/runner': 2.0.x - 3.0.x 321 | '@vitest/snapshot': 2.0.x - 3.0.x 322 | vitest: 2.0.x - 3.0.x 323 | 324 | '@cloudflare/workerd-darwin-64@1.20250321.0': 325 | resolution: {integrity: sha512-y273GfLaNCxkL8hTfo0c8FZKkOPdq+CPZAKJXPWB+YpS1JCOULu6lNTptpD7ZtF14dTYPkn5Weug31TTlviJmw==} 326 | engines: {node: '>=16'} 327 | cpu: [x64] 328 | os: [darwin] 329 | 330 | '@cloudflare/workerd-darwin-arm64@1.20250321.0': 331 | resolution: {integrity: sha512-qvf7/gkkQq7fAsoMlntJSimN/WfwQqxi2oL0aWZMGodTvs/yRHO2I4oE0eOihVdK1BXyBHJXNxEvNDBjF0+Yuw==} 332 | engines: {node: '>=16'} 333 | cpu: [arm64] 334 | os: [darwin] 335 | 336 | '@cloudflare/workerd-linux-64@1.20250321.0': 337 | resolution: {integrity: sha512-AEp3xjWFrNPO/h0StCOgOb0bWCcNThnkESpy91Wf4mfUF2p7tOCdp37Nk/1QIRqSxnfv4Hgxyi7gcWud9cJuMw==} 338 | engines: {node: '>=16'} 339 | cpu: [x64] 340 | os: [linux] 341 | 342 | '@cloudflare/workerd-linux-arm64@1.20250321.0': 343 | resolution: {integrity: sha512-wRWyMIoPIS1UBXCisW0FYTgGsfZD4AVS0hXA5nuLc0c21CvzZpmmTjqEWMcwPFenwy/MNL61NautVOC4qJqQ3Q==} 344 | engines: {node: '>=16'} 345 | cpu: [arm64] 346 | os: [linux] 347 | 348 | '@cloudflare/workerd-windows-64@1.20250321.0': 349 | resolution: {integrity: sha512-8vYP3QYO0zo2faUDfWl88jjfUvz7Si9GS3mUYaTh/TR9LcAUtsO7muLxPamqEyoxNFtbQgy08R4rTid94KRi3w==} 350 | engines: {node: '>=16'} 351 | cpu: [x64] 352 | os: [win32] 353 | 354 | '@cloudflare/workers-types@4.20250327.0': 355 | resolution: {integrity: sha512-rkoGnSY/GgBLCuhjZMIC3mt0jjqqvL17uOK92OI4eivmE+pMFOAchowDxIWOzDyYe5vwNCakbCeIM/FrSmwGJA==} 356 | 357 | '@cspotcode/source-map-support@0.8.1': 358 | resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} 359 | engines: {node: '>=12'} 360 | 361 | '@emnapi/runtime@1.3.1': 362 | resolution: {integrity: sha512-kEBmG8KyqtxJZv+ygbEim+KCGtIq1fC22Ms3S4ziXmYKm8uyoLX0MHONVKwp+9opg390VaKRNt4a7A9NwmpNhw==} 363 | 364 | '@esbuild/aix-ppc64@0.24.2': 365 | resolution: {integrity: sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==} 366 | engines: {node: '>=18'} 367 | cpu: [ppc64] 368 | os: [aix] 369 | 370 | '@esbuild/aix-ppc64@0.25.2': 371 | resolution: {integrity: sha512-wCIboOL2yXZym2cgm6mlA742s9QeJ8DjGVaL39dLN4rRwrOgOyYSnOaFPhKZGLb2ngj4EyfAFjsNJwPXZvseag==} 372 | engines: {node: '>=18'} 373 | cpu: [ppc64] 374 | os: [aix] 375 | 376 | '@esbuild/android-arm64@0.24.2': 377 | resolution: {integrity: sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg==} 378 | engines: {node: '>=18'} 379 | cpu: [arm64] 380 | os: [android] 381 | 382 | '@esbuild/android-arm64@0.25.2': 383 | resolution: {integrity: sha512-5ZAX5xOmTligeBaeNEPnPaeEuah53Id2tX4c2CVP3JaROTH+j4fnfHCkr1PjXMd78hMst+TlkfKcW/DlTq0i4w==} 384 | engines: {node: '>=18'} 385 | cpu: [arm64] 386 | os: [android] 387 | 388 | '@esbuild/android-arm@0.24.2': 389 | resolution: {integrity: sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q==} 390 | engines: {node: '>=18'} 391 | cpu: [arm] 392 | os: [android] 393 | 394 | '@esbuild/android-arm@0.25.2': 395 | resolution: {integrity: sha512-NQhH7jFstVY5x8CKbcfa166GoV0EFkaPkCKBQkdPJFvo5u+nGXLEH/ooniLb3QI8Fk58YAx7nsPLozUWfCBOJA==} 396 | engines: {node: '>=18'} 397 | cpu: [arm] 398 | os: [android] 399 | 400 | '@esbuild/android-x64@0.24.2': 401 | resolution: {integrity: sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw==} 402 | engines: {node: '>=18'} 403 | cpu: [x64] 404 | os: [android] 405 | 406 | '@esbuild/android-x64@0.25.2': 407 | resolution: {integrity: sha512-Ffcx+nnma8Sge4jzddPHCZVRvIfQ0kMsUsCMcJRHkGJ1cDmhe4SsrYIjLUKn1xpHZybmOqCWwB0zQvsjdEHtkg==} 408 | engines: {node: '>=18'} 409 | cpu: [x64] 410 | os: [android] 411 | 412 | '@esbuild/darwin-arm64@0.24.2': 413 | resolution: {integrity: sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA==} 414 | engines: {node: '>=18'} 415 | cpu: [arm64] 416 | os: [darwin] 417 | 418 | '@esbuild/darwin-arm64@0.25.2': 419 | resolution: {integrity: sha512-MpM6LUVTXAzOvN4KbjzU/q5smzryuoNjlriAIx+06RpecwCkL9JpenNzpKd2YMzLJFOdPqBpuub6eVRP5IgiSA==} 420 | engines: {node: '>=18'} 421 | cpu: [arm64] 422 | os: [darwin] 423 | 424 | '@esbuild/darwin-x64@0.24.2': 425 | resolution: {integrity: sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA==} 426 | engines: {node: '>=18'} 427 | cpu: [x64] 428 | os: [darwin] 429 | 430 | '@esbuild/darwin-x64@0.25.2': 431 | resolution: {integrity: sha512-5eRPrTX7wFyuWe8FqEFPG2cU0+butQQVNcT4sVipqjLYQjjh8a8+vUTfgBKM88ObB85ahsnTwF7PSIt6PG+QkA==} 432 | engines: {node: '>=18'} 433 | cpu: [x64] 434 | os: [darwin] 435 | 436 | '@esbuild/freebsd-arm64@0.24.2': 437 | resolution: {integrity: sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg==} 438 | engines: {node: '>=18'} 439 | cpu: [arm64] 440 | os: [freebsd] 441 | 442 | '@esbuild/freebsd-arm64@0.25.2': 443 | resolution: {integrity: sha512-mLwm4vXKiQ2UTSX4+ImyiPdiHjiZhIaE9QvC7sw0tZ6HoNMjYAqQpGyui5VRIi5sGd+uWq940gdCbY3VLvsO1w==} 444 | engines: {node: '>=18'} 445 | cpu: [arm64] 446 | os: [freebsd] 447 | 448 | '@esbuild/freebsd-x64@0.24.2': 449 | resolution: {integrity: sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q==} 450 | engines: {node: '>=18'} 451 | cpu: [x64] 452 | os: [freebsd] 453 | 454 | '@esbuild/freebsd-x64@0.25.2': 455 | resolution: {integrity: sha512-6qyyn6TjayJSwGpm8J9QYYGQcRgc90nmfdUb0O7pp1s4lTY+9D0H9O02v5JqGApUyiHOtkz6+1hZNvNtEhbwRQ==} 456 | engines: {node: '>=18'} 457 | cpu: [x64] 458 | os: [freebsd] 459 | 460 | '@esbuild/linux-arm64@0.24.2': 461 | resolution: {integrity: sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg==} 462 | engines: {node: '>=18'} 463 | cpu: [arm64] 464 | os: [linux] 465 | 466 | '@esbuild/linux-arm64@0.25.2': 467 | resolution: {integrity: sha512-gq/sjLsOyMT19I8obBISvhoYiZIAaGF8JpeXu1u8yPv8BE5HlWYobmlsfijFIZ9hIVGYkbdFhEqC0NvM4kNO0g==} 468 | engines: {node: '>=18'} 469 | cpu: [arm64] 470 | os: [linux] 471 | 472 | '@esbuild/linux-arm@0.24.2': 473 | resolution: {integrity: sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA==} 474 | engines: {node: '>=18'} 475 | cpu: [arm] 476 | os: [linux] 477 | 478 | '@esbuild/linux-arm@0.25.2': 479 | resolution: {integrity: sha512-UHBRgJcmjJv5oeQF8EpTRZs/1knq6loLxTsjc3nxO9eXAPDLcWW55flrMVc97qFPbmZP31ta1AZVUKQzKTzb0g==} 480 | engines: {node: '>=18'} 481 | cpu: [arm] 482 | os: [linux] 483 | 484 | '@esbuild/linux-ia32@0.24.2': 485 | resolution: {integrity: sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw==} 486 | engines: {node: '>=18'} 487 | cpu: [ia32] 488 | os: [linux] 489 | 490 | '@esbuild/linux-ia32@0.25.2': 491 | resolution: {integrity: sha512-bBYCv9obgW2cBP+2ZWfjYTU+f5cxRoGGQ5SeDbYdFCAZpYWrfjjfYwvUpP8MlKbP0nwZ5gyOU/0aUzZ5HWPuvQ==} 492 | engines: {node: '>=18'} 493 | cpu: [ia32] 494 | os: [linux] 495 | 496 | '@esbuild/linux-loong64@0.24.2': 497 | resolution: {integrity: sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ==} 498 | engines: {node: '>=18'} 499 | cpu: [loong64] 500 | os: [linux] 501 | 502 | '@esbuild/linux-loong64@0.25.2': 503 | resolution: {integrity: sha512-SHNGiKtvnU2dBlM5D8CXRFdd+6etgZ9dXfaPCeJtz+37PIUlixvlIhI23L5khKXs3DIzAn9V8v+qb1TRKrgT5w==} 504 | engines: {node: '>=18'} 505 | cpu: [loong64] 506 | os: [linux] 507 | 508 | '@esbuild/linux-mips64el@0.24.2': 509 | resolution: {integrity: sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw==} 510 | engines: {node: '>=18'} 511 | cpu: [mips64el] 512 | os: [linux] 513 | 514 | '@esbuild/linux-mips64el@0.25.2': 515 | resolution: {integrity: sha512-hDDRlzE6rPeoj+5fsADqdUZl1OzqDYow4TB4Y/3PlKBD0ph1e6uPHzIQcv2Z65u2K0kpeByIyAjCmjn1hJgG0Q==} 516 | engines: {node: '>=18'} 517 | cpu: [mips64el] 518 | os: [linux] 519 | 520 | '@esbuild/linux-ppc64@0.24.2': 521 | resolution: {integrity: sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw==} 522 | engines: {node: '>=18'} 523 | cpu: [ppc64] 524 | os: [linux] 525 | 526 | '@esbuild/linux-ppc64@0.25.2': 527 | resolution: {integrity: sha512-tsHu2RRSWzipmUi9UBDEzc0nLc4HtpZEI5Ba+Omms5456x5WaNuiG3u7xh5AO6sipnJ9r4cRWQB2tUjPyIkc6g==} 528 | engines: {node: '>=18'} 529 | cpu: [ppc64] 530 | os: [linux] 531 | 532 | '@esbuild/linux-riscv64@0.24.2': 533 | resolution: {integrity: sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q==} 534 | engines: {node: '>=18'} 535 | cpu: [riscv64] 536 | os: [linux] 537 | 538 | '@esbuild/linux-riscv64@0.25.2': 539 | resolution: {integrity: sha512-k4LtpgV7NJQOml/10uPU0s4SAXGnowi5qBSjaLWMojNCUICNu7TshqHLAEbkBdAszL5TabfvQ48kK84hyFzjnw==} 540 | engines: {node: '>=18'} 541 | cpu: [riscv64] 542 | os: [linux] 543 | 544 | '@esbuild/linux-s390x@0.24.2': 545 | resolution: {integrity: sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw==} 546 | engines: {node: '>=18'} 547 | cpu: [s390x] 548 | os: [linux] 549 | 550 | '@esbuild/linux-s390x@0.25.2': 551 | resolution: {integrity: sha512-GRa4IshOdvKY7M/rDpRR3gkiTNp34M0eLTaC1a08gNrh4u488aPhuZOCpkF6+2wl3zAN7L7XIpOFBhnaE3/Q8Q==} 552 | engines: {node: '>=18'} 553 | cpu: [s390x] 554 | os: [linux] 555 | 556 | '@esbuild/linux-x64@0.24.2': 557 | resolution: {integrity: sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q==} 558 | engines: {node: '>=18'} 559 | cpu: [x64] 560 | os: [linux] 561 | 562 | '@esbuild/linux-x64@0.25.2': 563 | resolution: {integrity: sha512-QInHERlqpTTZ4FRB0fROQWXcYRD64lAoiegezDunLpalZMjcUcld3YzZmVJ2H/Cp0wJRZ8Xtjtj0cEHhYc/uUg==} 564 | engines: {node: '>=18'} 565 | cpu: [x64] 566 | os: [linux] 567 | 568 | '@esbuild/netbsd-arm64@0.24.2': 569 | resolution: {integrity: sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw==} 570 | engines: {node: '>=18'} 571 | cpu: [arm64] 572 | os: [netbsd] 573 | 574 | '@esbuild/netbsd-arm64@0.25.2': 575 | resolution: {integrity: sha512-talAIBoY5M8vHc6EeI2WW9d/CkiO9MQJ0IOWX8hrLhxGbro/vBXJvaQXefW2cP0z0nQVTdQ/eNyGFV1GSKrxfw==} 576 | engines: {node: '>=18'} 577 | cpu: [arm64] 578 | os: [netbsd] 579 | 580 | '@esbuild/netbsd-x64@0.24.2': 581 | resolution: {integrity: sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw==} 582 | engines: {node: '>=18'} 583 | cpu: [x64] 584 | os: [netbsd] 585 | 586 | '@esbuild/netbsd-x64@0.25.2': 587 | resolution: {integrity: sha512-voZT9Z+tpOxrvfKFyfDYPc4DO4rk06qamv1a/fkuzHpiVBMOhpjK+vBmWM8J1eiB3OLSMFYNaOaBNLXGChf5tg==} 588 | engines: {node: '>=18'} 589 | cpu: [x64] 590 | os: [netbsd] 591 | 592 | '@esbuild/openbsd-arm64@0.24.2': 593 | resolution: {integrity: sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A==} 594 | engines: {node: '>=18'} 595 | cpu: [arm64] 596 | os: [openbsd] 597 | 598 | '@esbuild/openbsd-arm64@0.25.2': 599 | resolution: {integrity: sha512-dcXYOC6NXOqcykeDlwId9kB6OkPUxOEqU+rkrYVqJbK2hagWOMrsTGsMr8+rW02M+d5Op5NNlgMmjzecaRf7Tg==} 600 | engines: {node: '>=18'} 601 | cpu: [arm64] 602 | os: [openbsd] 603 | 604 | '@esbuild/openbsd-x64@0.24.2': 605 | resolution: {integrity: sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA==} 606 | engines: {node: '>=18'} 607 | cpu: [x64] 608 | os: [openbsd] 609 | 610 | '@esbuild/openbsd-x64@0.25.2': 611 | resolution: {integrity: sha512-t/TkWwahkH0Tsgoq1Ju7QfgGhArkGLkF1uYz8nQS/PPFlXbP5YgRpqQR3ARRiC2iXoLTWFxc6DJMSK10dVXluw==} 612 | engines: {node: '>=18'} 613 | cpu: [x64] 614 | os: [openbsd] 615 | 616 | '@esbuild/sunos-x64@0.24.2': 617 | resolution: {integrity: sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig==} 618 | engines: {node: '>=18'} 619 | cpu: [x64] 620 | os: [sunos] 621 | 622 | '@esbuild/sunos-x64@0.25.2': 623 | resolution: {integrity: sha512-cfZH1co2+imVdWCjd+D1gf9NjkchVhhdpgb1q5y6Hcv9TP6Zi9ZG/beI3ig8TvwT9lH9dlxLq5MQBBgwuj4xvA==} 624 | engines: {node: '>=18'} 625 | cpu: [x64] 626 | os: [sunos] 627 | 628 | '@esbuild/win32-arm64@0.24.2': 629 | resolution: {integrity: sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ==} 630 | engines: {node: '>=18'} 631 | cpu: [arm64] 632 | os: [win32] 633 | 634 | '@esbuild/win32-arm64@0.25.2': 635 | resolution: {integrity: sha512-7Loyjh+D/Nx/sOTzV8vfbB3GJuHdOQyrOryFdZvPHLf42Tk9ivBU5Aedi7iyX+x6rbn2Mh68T4qq1SDqJBQO5Q==} 636 | engines: {node: '>=18'} 637 | cpu: [arm64] 638 | os: [win32] 639 | 640 | '@esbuild/win32-ia32@0.24.2': 641 | resolution: {integrity: sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA==} 642 | engines: {node: '>=18'} 643 | cpu: [ia32] 644 | os: [win32] 645 | 646 | '@esbuild/win32-ia32@0.25.2': 647 | resolution: {integrity: sha512-WRJgsz9un0nqZJ4MfhabxaD9Ft8KioqU3JMinOTvobbX6MOSUigSBlogP8QB3uxpJDsFS6yN+3FDBdqE5lg9kg==} 648 | engines: {node: '>=18'} 649 | cpu: [ia32] 650 | os: [win32] 651 | 652 | '@esbuild/win32-x64@0.24.2': 653 | resolution: {integrity: sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg==} 654 | engines: {node: '>=18'} 655 | cpu: [x64] 656 | os: [win32] 657 | 658 | '@esbuild/win32-x64@0.25.2': 659 | resolution: {integrity: sha512-kM3HKb16VIXZyIeVrM1ygYmZBKybX8N4p754bw390wGO3Tf2j4L2/WYL+4suWujpgf6GBYs3jv7TyUivdd05JA==} 660 | engines: {node: '>=18'} 661 | cpu: [x64] 662 | os: [win32] 663 | 664 | '@fastify/busboy@2.1.1': 665 | resolution: {integrity: sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==} 666 | engines: {node: '>=14'} 667 | 668 | '@img/sharp-darwin-arm64@0.33.5': 669 | resolution: {integrity: sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==} 670 | engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} 671 | cpu: [arm64] 672 | os: [darwin] 673 | 674 | '@img/sharp-darwin-x64@0.33.5': 675 | resolution: {integrity: sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==} 676 | engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} 677 | cpu: [x64] 678 | os: [darwin] 679 | 680 | '@img/sharp-libvips-darwin-arm64@1.0.4': 681 | resolution: {integrity: sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==} 682 | cpu: [arm64] 683 | os: [darwin] 684 | 685 | '@img/sharp-libvips-darwin-x64@1.0.4': 686 | resolution: {integrity: sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==} 687 | cpu: [x64] 688 | os: [darwin] 689 | 690 | '@img/sharp-libvips-linux-arm64@1.0.4': 691 | resolution: {integrity: sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==} 692 | cpu: [arm64] 693 | os: [linux] 694 | 695 | '@img/sharp-libvips-linux-arm@1.0.5': 696 | resolution: {integrity: sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==} 697 | cpu: [arm] 698 | os: [linux] 699 | 700 | '@img/sharp-libvips-linux-s390x@1.0.4': 701 | resolution: {integrity: sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==} 702 | cpu: [s390x] 703 | os: [linux] 704 | 705 | '@img/sharp-libvips-linux-x64@1.0.4': 706 | resolution: {integrity: sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==} 707 | cpu: [x64] 708 | os: [linux] 709 | 710 | '@img/sharp-libvips-linuxmusl-arm64@1.0.4': 711 | resolution: {integrity: sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==} 712 | cpu: [arm64] 713 | os: [linux] 714 | 715 | '@img/sharp-libvips-linuxmusl-x64@1.0.4': 716 | resolution: {integrity: sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==} 717 | cpu: [x64] 718 | os: [linux] 719 | 720 | '@img/sharp-linux-arm64@0.33.5': 721 | resolution: {integrity: sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==} 722 | engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} 723 | cpu: [arm64] 724 | os: [linux] 725 | 726 | '@img/sharp-linux-arm@0.33.5': 727 | resolution: {integrity: sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==} 728 | engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} 729 | cpu: [arm] 730 | os: [linux] 731 | 732 | '@img/sharp-linux-s390x@0.33.5': 733 | resolution: {integrity: sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==} 734 | engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} 735 | cpu: [s390x] 736 | os: [linux] 737 | 738 | '@img/sharp-linux-x64@0.33.5': 739 | resolution: {integrity: sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==} 740 | engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} 741 | cpu: [x64] 742 | os: [linux] 743 | 744 | '@img/sharp-linuxmusl-arm64@0.33.5': 745 | resolution: {integrity: sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==} 746 | engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} 747 | cpu: [arm64] 748 | os: [linux] 749 | 750 | '@img/sharp-linuxmusl-x64@0.33.5': 751 | resolution: {integrity: sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==} 752 | engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} 753 | cpu: [x64] 754 | os: [linux] 755 | 756 | '@img/sharp-wasm32@0.33.5': 757 | resolution: {integrity: sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==} 758 | engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} 759 | cpu: [wasm32] 760 | 761 | '@img/sharp-win32-ia32@0.33.5': 762 | resolution: {integrity: sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==} 763 | engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} 764 | cpu: [ia32] 765 | os: [win32] 766 | 767 | '@img/sharp-win32-x64@0.33.5': 768 | resolution: {integrity: sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==} 769 | engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} 770 | cpu: [x64] 771 | os: [win32] 772 | 773 | '@jridgewell/resolve-uri@3.1.2': 774 | resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} 775 | engines: {node: '>=6.0.0'} 776 | 777 | '@jridgewell/sourcemap-codec@1.5.0': 778 | resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} 779 | 780 | '@jridgewell/trace-mapping@0.3.9': 781 | resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} 782 | 783 | '@rollup/rollup-android-arm-eabi@4.38.0': 784 | resolution: {integrity: sha512-ldomqc4/jDZu/xpYU+aRxo3V4mGCV9HeTgUBANI3oIQMOL+SsxB+S2lxMpkFp5UamSS3XuTMQVbsS24R4J4Qjg==} 785 | cpu: [arm] 786 | os: [android] 787 | 788 | '@rollup/rollup-android-arm64@4.38.0': 789 | resolution: {integrity: sha512-VUsgcy4GhhT7rokwzYQP+aV9XnSLkkhlEJ0St8pbasuWO/vwphhZQxYEKUP3ayeCYLhk6gEtacRpYP/cj3GjyQ==} 790 | cpu: [arm64] 791 | os: [android] 792 | 793 | '@rollup/rollup-darwin-arm64@4.38.0': 794 | resolution: {integrity: sha512-buA17AYXlW9Rn091sWMq1xGUvWQFOH4N1rqUxGJtEQzhChxWjldGCCup7r/wUnaI6Au8sKXpoh0xg58a7cgcpg==} 795 | cpu: [arm64] 796 | os: [darwin] 797 | 798 | '@rollup/rollup-darwin-x64@4.38.0': 799 | resolution: {integrity: sha512-Mgcmc78AjunP1SKXl624vVBOF2bzwNWFPMP4fpOu05vS0amnLcX8gHIge7q/lDAHy3T2HeR0TqrriZDQS2Woeg==} 800 | cpu: [x64] 801 | os: [darwin] 802 | 803 | '@rollup/rollup-freebsd-arm64@4.38.0': 804 | resolution: {integrity: sha512-zzJACgjLbQTsscxWqvrEQAEh28hqhebpRz5q/uUd1T7VTwUNZ4VIXQt5hE7ncs0GrF+s7d3S4on4TiXUY8KoQA==} 805 | cpu: [arm64] 806 | os: [freebsd] 807 | 808 | '@rollup/rollup-freebsd-x64@4.38.0': 809 | resolution: {integrity: sha512-hCY/KAeYMCyDpEE4pTETam0XZS4/5GXzlLgpi5f0IaPExw9kuB+PDTOTLuPtM10TlRG0U9OSmXJ+Wq9J39LvAg==} 810 | cpu: [x64] 811 | os: [freebsd] 812 | 813 | '@rollup/rollup-linux-arm-gnueabihf@4.38.0': 814 | resolution: {integrity: sha512-mimPH43mHl4JdOTD7bUMFhBdrg6f9HzMTOEnzRmXbOZqjijCw8LA5z8uL6LCjxSa67H2xiLFvvO67PT05PRKGg==} 815 | cpu: [arm] 816 | os: [linux] 817 | 818 | '@rollup/rollup-linux-arm-musleabihf@4.38.0': 819 | resolution: {integrity: sha512-tPiJtiOoNuIH8XGG8sWoMMkAMm98PUwlriOFCCbZGc9WCax+GLeVRhmaxjJtz6WxrPKACgrwoZ5ia/uapq3ZVg==} 820 | cpu: [arm] 821 | os: [linux] 822 | 823 | '@rollup/rollup-linux-arm64-gnu@4.38.0': 824 | resolution: {integrity: sha512-wZco59rIVuB0tjQS0CSHTTUcEde+pXQWugZVxWaQFdQQ1VYub/sTrNdY76D1MKdN2NB48JDuGABP6o6fqos8mA==} 825 | cpu: [arm64] 826 | os: [linux] 827 | 828 | '@rollup/rollup-linux-arm64-musl@4.38.0': 829 | resolution: {integrity: sha512-fQgqwKmW0REM4LomQ+87PP8w8xvU9LZfeLBKybeli+0yHT7VKILINzFEuggvnV9M3x1Ed4gUBmGUzCo/ikmFbQ==} 830 | cpu: [arm64] 831 | os: [linux] 832 | 833 | '@rollup/rollup-linux-loongarch64-gnu@4.38.0': 834 | resolution: {integrity: sha512-hz5oqQLXTB3SbXpfkKHKXLdIp02/w3M+ajp8p4yWOWwQRtHWiEOCKtc9U+YXahrwdk+3qHdFMDWR5k+4dIlddg==} 835 | cpu: [loong64] 836 | os: [linux] 837 | 838 | '@rollup/rollup-linux-powerpc64le-gnu@4.38.0': 839 | resolution: {integrity: sha512-NXqygK/dTSibQ+0pzxsL3r4Xl8oPqVoWbZV9niqOnIHV/J92fe65pOir0xjkUZDRSPyFRvu+4YOpJF9BZHQImw==} 840 | cpu: [ppc64] 841 | os: [linux] 842 | 843 | '@rollup/rollup-linux-riscv64-gnu@4.38.0': 844 | resolution: {integrity: sha512-GEAIabR1uFyvf/jW/5jfu8gjM06/4kZ1W+j1nWTSSB3w6moZEBm7iBtzwQ3a1Pxos2F7Gz+58aVEnZHU295QTg==} 845 | cpu: [riscv64] 846 | os: [linux] 847 | 848 | '@rollup/rollup-linux-riscv64-musl@4.38.0': 849 | resolution: {integrity: sha512-9EYTX+Gus2EGPbfs+fh7l95wVADtSQyYw4DfSBcYdUEAmP2lqSZY0Y17yX/3m5VKGGJ4UmIH5LHLkMJft3bYoA==} 850 | cpu: [riscv64] 851 | os: [linux] 852 | 853 | '@rollup/rollup-linux-s390x-gnu@4.38.0': 854 | resolution: {integrity: sha512-Mpp6+Z5VhB9VDk7RwZXoG2qMdERm3Jw07RNlXHE0bOnEeX+l7Fy4bg+NxfyN15ruuY3/7Vrbpm75J9QHFqj5+Q==} 855 | cpu: [s390x] 856 | os: [linux] 857 | 858 | '@rollup/rollup-linux-x64-gnu@4.38.0': 859 | resolution: {integrity: sha512-vPvNgFlZRAgO7rwncMeE0+8c4Hmc+qixnp00/Uv3ht2x7KYrJ6ERVd3/R0nUtlE6/hu7/HiiNHJ/rP6knRFt1w==} 860 | cpu: [x64] 861 | os: [linux] 862 | 863 | '@rollup/rollup-linux-x64-musl@4.38.0': 864 | resolution: {integrity: sha512-q5Zv+goWvQUGCaL7fU8NuTw8aydIL/C9abAVGCzRReuj5h30TPx4LumBtAidrVOtXnlB+RZkBtExMsfqkMfb8g==} 865 | cpu: [x64] 866 | os: [linux] 867 | 868 | '@rollup/rollup-win32-arm64-msvc@4.38.0': 869 | resolution: {integrity: sha512-u/Jbm1BU89Vftqyqbmxdq14nBaQjQX1HhmsdBWqSdGClNaKwhjsg5TpW+5Ibs1mb8Es9wJiMdl86BcmtUVXNZg==} 870 | cpu: [arm64] 871 | os: [win32] 872 | 873 | '@rollup/rollup-win32-ia32-msvc@4.38.0': 874 | resolution: {integrity: sha512-mqu4PzTrlpNHHbu5qleGvXJoGgHpChBlrBx/mEhTPpnAL1ZAYFlvHD7rLK839LLKQzqEQMFJfGrrOHItN4ZQqA==} 875 | cpu: [ia32] 876 | os: [win32] 877 | 878 | '@rollup/rollup-win32-x64-msvc@4.38.0': 879 | resolution: {integrity: sha512-jjqy3uWlecfB98Psxb5cD6Fny9Fupv9LrDSPTQZUROqjvZmcCqNu4UMl7qqhlUUGpwiAkotj6GYu4SZdcr/nLw==} 880 | cpu: [x64] 881 | os: [win32] 882 | 883 | '@types/estree@1.0.7': 884 | resolution: {integrity: sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==} 885 | 886 | '@types/node@20.14.2': 887 | resolution: {integrity: sha512-xyu6WAMVwv6AKFLB+e/7ySZVr/0zLCzOa7rSpq6jNwpqOrUbcACDWC+53d4n2QHOnDou0fbIsg8wZu/sxrnI4Q==} 888 | 889 | '@vitest/expect@3.0.8': 890 | resolution: {integrity: sha512-Xu6TTIavTvSSS6LZaA3EebWFr6tsoXPetOWNMOlc7LO88QVVBwq2oQWBoDiLCN6YTvNYsGSjqOO8CAdjom5DCQ==} 891 | 892 | '@vitest/mocker@3.0.8': 893 | resolution: {integrity: sha512-n3LjS7fcW1BCoF+zWZxG7/5XvuYH+lsFg+BDwwAz0arIwHQJFUEsKBQ0BLU49fCxuM/2HSeBPHQD8WjgrxMfow==} 894 | peerDependencies: 895 | msw: ^2.4.9 896 | vite: ^5.0.0 || ^6.0.0 897 | peerDependenciesMeta: 898 | msw: 899 | optional: true 900 | vite: 901 | optional: true 902 | 903 | '@vitest/pretty-format@3.0.8': 904 | resolution: {integrity: sha512-BNqwbEyitFhzYMYHUVbIvepOyeQOSFA/NeJMIP9enMntkkxLgOcgABH6fjyXG85ipTgvero6noreavGIqfJcIg==} 905 | 906 | '@vitest/pretty-format@3.0.9': 907 | resolution: {integrity: sha512-OW9F8t2J3AwFEwENg3yMyKWweF7oRJlMyHOMIhO5F3n0+cgQAJZBjNgrF8dLwFTEXl5jUqBLXd9QyyKv8zEcmA==} 908 | 909 | '@vitest/runner@3.0.8': 910 | resolution: {integrity: sha512-c7UUw6gEcOzI8fih+uaAXS5DwjlBaCJUo7KJ4VvJcjL95+DSR1kova2hFuRt3w41KZEFcOEiq098KkyrjXeM5w==} 911 | 912 | '@vitest/runner@3.0.9': 913 | resolution: {integrity: sha512-NX9oUXgF9HPfJSwl8tUZCMP1oGx2+Sf+ru6d05QjzQz4OwWg0psEzwY6VexP2tTHWdOkhKHUIZH+fS6nA7jfOw==} 914 | 915 | '@vitest/snapshot@3.0.8': 916 | resolution: {integrity: sha512-x8IlMGSEMugakInj44nUrLSILh/zy1f2/BgH0UeHpNyOocG18M9CWVIFBaXPt8TrqVZWmcPjwfG/ht5tnpba8A==} 917 | 918 | '@vitest/snapshot@3.0.9': 919 | resolution: {integrity: sha512-AiLUiuZ0FuA+/8i19mTYd+re5jqjEc2jZbgJ2up0VY0Ddyyxg/uUtBDpIFAy4uzKaQxOW8gMgBdAJJ2ydhu39A==} 920 | 921 | '@vitest/spy@3.0.8': 922 | resolution: {integrity: sha512-MR+PzJa+22vFKYb934CejhR4BeRpMSoxkvNoDit68GQxRLSf11aT6CTj3XaqUU9rxgWJFnqicN/wxw6yBRkI1Q==} 923 | 924 | '@vitest/utils@3.0.8': 925 | resolution: {integrity: sha512-nkBC3aEhfX2PdtQI/QwAWp8qZWwzASsU4Npbcd5RdMPBSSLCpkZp52P3xku3s3uA0HIEhGvEcF8rNkBsz9dQ4Q==} 926 | 927 | '@vitest/utils@3.0.9': 928 | resolution: {integrity: sha512-ilHM5fHhZ89MCp5aAaM9uhfl1c2JdxVxl3McqsdVyVNN6JffnEen8UMCdRTzOhGXNQGo5GNL9QugHrz727Wnng==} 929 | 930 | acorn-walk@8.3.2: 931 | resolution: {integrity: sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==} 932 | engines: {node: '>=0.4.0'} 933 | 934 | acorn@8.14.0: 935 | resolution: {integrity: sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==} 936 | engines: {node: '>=0.4.0'} 937 | hasBin: true 938 | 939 | as-table@1.0.55: 940 | resolution: {integrity: sha512-xvsWESUJn0JN421Xb9MQw6AsMHRCUknCe0Wjlxvjud80mU4E6hQf1A6NzQKcYNmYw62MfzEtXc+badstZP3JpQ==} 941 | 942 | assertion-error@2.0.1: 943 | resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} 944 | engines: {node: '>=12'} 945 | 946 | birpc@0.2.14: 947 | resolution: {integrity: sha512-37FHE8rqsYM5JEKCnXFyHpBCzvgHEExwVVTq+nUmloInU7l8ezD1TpOhKpS8oe1DTYFqEK27rFZVKG43oTqXRA==} 948 | 949 | blake3-wasm@2.1.5: 950 | resolution: {integrity: sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==} 951 | 952 | cac@6.7.14: 953 | resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} 954 | engines: {node: '>=8'} 955 | 956 | chai@5.2.0: 957 | resolution: {integrity: sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw==} 958 | engines: {node: '>=12'} 959 | 960 | check-error@2.1.1: 961 | resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==} 962 | engines: {node: '>= 16'} 963 | 964 | cjs-module-lexer@1.4.3: 965 | resolution: {integrity: sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==} 966 | 967 | color-convert@2.0.1: 968 | resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} 969 | engines: {node: '>=7.0.0'} 970 | 971 | color-name@1.1.4: 972 | resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} 973 | 974 | color-string@1.9.1: 975 | resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==} 976 | 977 | color@4.2.3: 978 | resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==} 979 | engines: {node: '>=12.5.0'} 980 | 981 | cookie@0.5.0: 982 | resolution: {integrity: sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==} 983 | engines: {node: '>= 0.6'} 984 | 985 | data-uri-to-buffer@2.0.2: 986 | resolution: {integrity: sha512-ND9qDTLc6diwj+Xe5cdAgVTbLVdXbtxTJRXRhli8Mowuaan+0EJOtdqJ0QCHNSSPyoXGx9HX2/VMnKeC34AChA==} 987 | 988 | debug@4.4.0: 989 | resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==} 990 | engines: {node: '>=6.0'} 991 | peerDependencies: 992 | supports-color: '*' 993 | peerDependenciesMeta: 994 | supports-color: 995 | optional: true 996 | 997 | deep-eql@5.0.2: 998 | resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} 999 | engines: {node: '>=6'} 1000 | 1001 | defu@6.1.4: 1002 | resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==} 1003 | 1004 | detect-libc@2.0.3: 1005 | resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==} 1006 | engines: {node: '>=8'} 1007 | 1008 | devalue@4.3.3: 1009 | resolution: {integrity: sha512-UH8EL6H2ifcY8TbD2QsxwCC/pr5xSwPvv85LrLXVihmHVC3T3YqTCIwnR5ak0yO1KYqlxrPVOA/JVZJYPy2ATg==} 1010 | 1011 | es-module-lexer@1.6.0: 1012 | resolution: {integrity: sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ==} 1013 | 1014 | esbuild@0.24.2: 1015 | resolution: {integrity: sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==} 1016 | engines: {node: '>=18'} 1017 | hasBin: true 1018 | 1019 | esbuild@0.25.2: 1020 | resolution: {integrity: sha512-16854zccKPnC+toMywC+uKNeYSv+/eXkevRAfwRD/G9Cleq66m8XFIrigkbvauLLlCfDL45Q2cWegSg53gGBnQ==} 1021 | engines: {node: '>=18'} 1022 | hasBin: true 1023 | 1024 | estree-walker@3.0.3: 1025 | resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} 1026 | 1027 | exit-hook@2.2.1: 1028 | resolution: {integrity: sha512-eNTPlAD67BmP31LDINZ3U7HSF8l57TxOY2PmBJ1shpCvpnxBF93mWCE8YHBnXs8qiUZJc9WDcWIeC3a2HIAMfw==} 1029 | engines: {node: '>=6'} 1030 | 1031 | expect-type@1.2.0: 1032 | resolution: {integrity: sha512-80F22aiJ3GLyVnS/B3HzgR6RelZVumzj9jkL0Rhz4h0xYbNW9PjlQz5h3J/SShErbXBc295vseR4/MIbVmUbeA==} 1033 | engines: {node: '>=12.0.0'} 1034 | 1035 | exsolve@1.0.4: 1036 | resolution: {integrity: sha512-xsZH6PXaER4XoV+NiT7JHp1bJodJVT+cxeSH1G0f0tlT0lJqYuHUP3bUx2HtfTDvOagMINYp8rsqusxud3RXhw==} 1037 | 1038 | fsevents@2.3.3: 1039 | resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} 1040 | engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} 1041 | os: [darwin] 1042 | 1043 | get-source@2.0.12: 1044 | resolution: {integrity: sha512-X5+4+iD+HoSeEED+uwrQ07BOQr0kEDFMVqqpBuI+RaZBpBpHCuXxo70bjar6f0b0u/DQJsJ7ssurpP0V60Az+w==} 1045 | 1046 | glob-to-regexp@0.4.1: 1047 | resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==} 1048 | 1049 | hono@4.7.5: 1050 | resolution: {integrity: sha512-fDOK5W2C1vZACsgLONigdZTRZxuBqFtcKh7bUQ5cVSbwI2RWjloJDcgFOVzbQrlI6pCmhlTsVYZ7zpLj4m4qMQ==} 1051 | engines: {node: '>=16.9.0'} 1052 | 1053 | is-arrayish@0.3.2: 1054 | resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==} 1055 | 1056 | itty-router@5.0.18: 1057 | resolution: {integrity: sha512-mK3ReOt4ARAGy0V0J7uHmArG2USN2x0zprZ+u+YgmeRjXTDbaowDy3kPcsmQY6tH+uHhDgpWit9Vqmv/4rTXwA==} 1058 | 1059 | jose@6.0.10: 1060 | resolution: {integrity: sha512-skIAxZqcMkOrSwjJvplIPYrlXGpxTPnro2/QWTDCxAdWQrSTV5/KqspMWmi5WAx5+ULswASJiZ0a+1B/Lxt9cw==} 1061 | 1062 | loupe@3.1.3: 1063 | resolution: {integrity: sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug==} 1064 | 1065 | magic-string@0.30.17: 1066 | resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} 1067 | 1068 | mime@3.0.0: 1069 | resolution: {integrity: sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==} 1070 | engines: {node: '>=10.0.0'} 1071 | hasBin: true 1072 | 1073 | miniflare@4.20250321.1: 1074 | resolution: {integrity: sha512-pQuVtF6vQ1zMvPCo3Z19mzSFjgnlEnybzNzAJZipsqIk6kMXpYBZq+d8cWmeQFkBYlgeZKeKJ4EBKT6KapfTNg==} 1075 | engines: {node: '>=18.0.0'} 1076 | hasBin: true 1077 | 1078 | ms@2.1.3: 1079 | resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} 1080 | 1081 | mustache@4.2.0: 1082 | resolution: {integrity: sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==} 1083 | hasBin: true 1084 | 1085 | nanoid@3.3.8: 1086 | resolution: {integrity: sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==} 1087 | engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} 1088 | hasBin: true 1089 | 1090 | ohash@2.0.11: 1091 | resolution: {integrity: sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==} 1092 | 1093 | path-to-regexp@6.3.0: 1094 | resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} 1095 | 1096 | pathe@2.0.3: 1097 | resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} 1098 | 1099 | pathval@2.0.0: 1100 | resolution: {integrity: sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==} 1101 | engines: {node: '>= 14.16'} 1102 | 1103 | picocolors@1.1.1: 1104 | resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} 1105 | 1106 | postcss@8.5.3: 1107 | resolution: {integrity: sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==} 1108 | engines: {node: ^10 || ^12 || >=14} 1109 | 1110 | printable-characters@1.0.42: 1111 | resolution: {integrity: sha512-dKp+C4iXWK4vVYZmYSd0KBH5F/h1HoZRsbJ82AVKRO3PEo8L4lBS/vLwhVtpwwuYcoIsVY+1JYKR268yn480uQ==} 1112 | 1113 | rollup@4.38.0: 1114 | resolution: {integrity: sha512-5SsIRtJy9bf1ErAOiFMFzl64Ex9X5V7bnJ+WlFMb+zmP459OSWCEG7b0ERZ+PEU7xPt4OG3RHbrp1LJlXxYTrw==} 1115 | engines: {node: '>=18.0.0', npm: '>=8.0.0'} 1116 | hasBin: true 1117 | 1118 | semver@7.7.1: 1119 | resolution: {integrity: sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==} 1120 | engines: {node: '>=10'} 1121 | hasBin: true 1122 | 1123 | sharp@0.33.5: 1124 | resolution: {integrity: sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==} 1125 | engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} 1126 | 1127 | short-unique-id@5.2.2: 1128 | resolution: {integrity: sha512-MlRVyT5RYfDO2kUzBgOPlZriRzG+NIAuwSy1HBN8tahXyFi3+804GGi/mzjUsi6VxgiQuDgMnhoI2FqmSHX8Tg==} 1129 | hasBin: true 1130 | 1131 | siginfo@2.0.0: 1132 | resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} 1133 | 1134 | simple-swizzle@0.2.2: 1135 | resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==} 1136 | 1137 | source-map-js@1.2.1: 1138 | resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} 1139 | engines: {node: '>=0.10.0'} 1140 | 1141 | source-map@0.6.1: 1142 | resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} 1143 | engines: {node: '>=0.10.0'} 1144 | 1145 | stackback@0.0.2: 1146 | resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} 1147 | 1148 | stacktracey@2.1.8: 1149 | resolution: {integrity: sha512-Kpij9riA+UNg7TnphqjH7/CzctQ/owJGNbFkfEeve4Z4uxT5+JapVLFXcsurIfN34gnTWZNJ/f7NMG0E8JDzTw==} 1150 | 1151 | std-env@3.8.1: 1152 | resolution: {integrity: sha512-vj5lIj3Mwf9D79hBkltk5qmkFI+biIKWS2IBxEyEU3AX1tUf7AoL8nSazCOiiqQsGKIq01SClsKEzweu34uwvA==} 1153 | 1154 | stoppable@1.1.0: 1155 | resolution: {integrity: sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==} 1156 | engines: {node: '>=4', npm: '>=6'} 1157 | 1158 | tinybench@2.9.0: 1159 | resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} 1160 | 1161 | tinyexec@0.3.2: 1162 | resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} 1163 | 1164 | tinypool@1.0.2: 1165 | resolution: {integrity: sha512-al6n+QEANGFOMf/dmUMsuS5/r9B06uwlyNjZZql/zv8J7ybHCgoihBNORZCY2mzUuAnomQa2JdhyHKzZxPCrFA==} 1166 | engines: {node: ^18.0.0 || >=20.0.0} 1167 | 1168 | tinyrainbow@2.0.0: 1169 | resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==} 1170 | engines: {node: '>=14.0.0'} 1171 | 1172 | tinyspy@3.0.2: 1173 | resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==} 1174 | engines: {node: '>=14.0.0'} 1175 | 1176 | tslib@2.8.1: 1177 | resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} 1178 | 1179 | typescript@5.8.2: 1180 | resolution: {integrity: sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==} 1181 | engines: {node: '>=14.17'} 1182 | hasBin: true 1183 | 1184 | ufo@1.5.4: 1185 | resolution: {integrity: sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ==} 1186 | 1187 | undici-types@5.26.5: 1188 | resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} 1189 | 1190 | undici@5.29.0: 1191 | resolution: {integrity: sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==} 1192 | engines: {node: '>=14.0'} 1193 | 1194 | unenv@2.0.0-rc.15: 1195 | resolution: {integrity: sha512-J/rEIZU8w6FOfLNz/hNKsnY+fFHWnu9MH4yRbSZF3xbbGHovcetXPs7sD+9p8L6CeNC//I9bhRYAOsBt2u7/OA==} 1196 | 1197 | vite-node@3.0.8: 1198 | resolution: {integrity: sha512-6PhR4H9VGlcwXZ+KWCdMqbtG649xCPZqfI9j2PsK1FcXgEzro5bGHcVKFCTqPLaNKZES8Evqv4LwvZARsq5qlg==} 1199 | engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} 1200 | hasBin: true 1201 | 1202 | vite@6.2.3: 1203 | resolution: {integrity: sha512-IzwM54g4y9JA/xAeBPNaDXiBF8Jsgl3VBQ2YQ/wOY6fyW3xMdSoltIV3Bo59DErdqdE6RxUfv8W69DvUorE4Eg==} 1204 | engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} 1205 | hasBin: true 1206 | peerDependencies: 1207 | '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 1208 | jiti: '>=1.21.0' 1209 | less: '*' 1210 | lightningcss: ^1.21.0 1211 | sass: '*' 1212 | sass-embedded: '*' 1213 | stylus: '*' 1214 | sugarss: '*' 1215 | terser: ^5.16.0 1216 | tsx: ^4.8.1 1217 | yaml: ^2.4.2 1218 | peerDependenciesMeta: 1219 | '@types/node': 1220 | optional: true 1221 | jiti: 1222 | optional: true 1223 | less: 1224 | optional: true 1225 | lightningcss: 1226 | optional: true 1227 | sass: 1228 | optional: true 1229 | sass-embedded: 1230 | optional: true 1231 | stylus: 1232 | optional: true 1233 | sugarss: 1234 | optional: true 1235 | terser: 1236 | optional: true 1237 | tsx: 1238 | optional: true 1239 | yaml: 1240 | optional: true 1241 | 1242 | vitest@3.0.8: 1243 | resolution: {integrity: sha512-dfqAsNqRGUc8hB9OVR2P0w8PZPEckti2+5rdZip0WIz9WW0MnImJ8XiR61QhqLa92EQzKP2uPkzenKOAHyEIbA==} 1244 | engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} 1245 | hasBin: true 1246 | peerDependencies: 1247 | '@edge-runtime/vm': '*' 1248 | '@types/debug': ^4.1.12 1249 | '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 1250 | '@vitest/browser': 3.0.8 1251 | '@vitest/ui': 3.0.8 1252 | happy-dom: '*' 1253 | jsdom: '*' 1254 | peerDependenciesMeta: 1255 | '@edge-runtime/vm': 1256 | optional: true 1257 | '@types/debug': 1258 | optional: true 1259 | '@types/node': 1260 | optional: true 1261 | '@vitest/browser': 1262 | optional: true 1263 | '@vitest/ui': 1264 | optional: true 1265 | happy-dom: 1266 | optional: true 1267 | jsdom: 1268 | optional: true 1269 | 1270 | why-is-node-running@2.3.0: 1271 | resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} 1272 | engines: {node: '>=8'} 1273 | hasBin: true 1274 | 1275 | workerd@1.20250321.0: 1276 | resolution: {integrity: sha512-vyuz9pdJ+7o1lC79vQ2UVRLXPARa2Lq94PbTfqEcYQeSxeR9X+YqhNq2yysv8Zs5vpokmexLCtMniPp9u+2LVQ==} 1277 | engines: {node: '>=16'} 1278 | hasBin: true 1279 | 1280 | wrangler@4.6.0: 1281 | resolution: {integrity: sha512-2a2ZD0adlvxQ1H+nRKkuuD0dkgaYTOPlC7gBolItk5TfqOSliEV4m6DtZtKtJp33ioM+Uy6TlEWPpA2TrDveEQ==} 1282 | engines: {node: '>=18.0.0'} 1283 | hasBin: true 1284 | peerDependencies: 1285 | '@cloudflare/workers-types': ^4.20250321.0 1286 | peerDependenciesMeta: 1287 | '@cloudflare/workers-types': 1288 | optional: true 1289 | 1290 | ws@8.18.0: 1291 | resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==} 1292 | engines: {node: '>=10.0.0'} 1293 | peerDependencies: 1294 | bufferutil: ^4.0.1 1295 | utf-8-validate: '>=5.0.2' 1296 | peerDependenciesMeta: 1297 | bufferutil: 1298 | optional: true 1299 | utf-8-validate: 1300 | optional: true 1301 | 1302 | youch@3.2.3: 1303 | resolution: {integrity: sha512-ZBcWz/uzZaQVdCvfV4uk616Bbpf2ee+F/AvuKDR5EwX/Y4v06xWdtMluqTD7+KlZdM93lLm9gMZYo0sKBS0pgw==} 1304 | 1305 | zod@3.22.3: 1306 | resolution: {integrity: sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug==} 1307 | 1308 | zod@3.24.2: 1309 | resolution: {integrity: sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==} 1310 | 1311 | snapshots: 1312 | 1313 | '@biomejs/biome@1.9.4': 1314 | optionalDependencies: 1315 | '@biomejs/cli-darwin-arm64': 1.9.4 1316 | '@biomejs/cli-darwin-x64': 1.9.4 1317 | '@biomejs/cli-linux-arm64': 1.9.4 1318 | '@biomejs/cli-linux-arm64-musl': 1.9.4 1319 | '@biomejs/cli-linux-x64': 1.9.4 1320 | '@biomejs/cli-linux-x64-musl': 1.9.4 1321 | '@biomejs/cli-win32-arm64': 1.9.4 1322 | '@biomejs/cli-win32-x64': 1.9.4 1323 | 1324 | '@biomejs/cli-darwin-arm64@1.9.4': 1325 | optional: true 1326 | 1327 | '@biomejs/cli-darwin-x64@1.9.4': 1328 | optional: true 1329 | 1330 | '@biomejs/cli-linux-arm64-musl@1.9.4': 1331 | optional: true 1332 | 1333 | '@biomejs/cli-linux-arm64@1.9.4': 1334 | optional: true 1335 | 1336 | '@biomejs/cli-linux-x64-musl@1.9.4': 1337 | optional: true 1338 | 1339 | '@biomejs/cli-linux-x64@1.9.4': 1340 | optional: true 1341 | 1342 | '@biomejs/cli-win32-arm64@1.9.4': 1343 | optional: true 1344 | 1345 | '@biomejs/cli-win32-x64@1.9.4': 1346 | optional: true 1347 | 1348 | '@cloudflare/kv-asset-handler@0.4.0': 1349 | dependencies: 1350 | mime: 3.0.0 1351 | 1352 | '@cloudflare/unenv-preset@2.3.1(unenv@2.0.0-rc.15)(workerd@1.20250321.0)': 1353 | dependencies: 1354 | unenv: 2.0.0-rc.15 1355 | optionalDependencies: 1356 | workerd: 1.20250321.0 1357 | 1358 | '@cloudflare/vitest-pool-workers@0.8.8(@cloudflare/workers-types@4.20250327.0)(@vitest/runner@3.0.9)(@vitest/snapshot@3.0.9)(vitest@3.0.8(@types/node@20.14.2))': 1359 | dependencies: 1360 | '@vitest/runner': 3.0.9 1361 | '@vitest/snapshot': 3.0.9 1362 | birpc: 0.2.14 1363 | cjs-module-lexer: 1.4.3 1364 | devalue: 4.3.3 1365 | esbuild: 0.24.2 1366 | miniflare: 4.20250321.1 1367 | semver: 7.7.1 1368 | vitest: 3.0.8(@types/node@20.14.2) 1369 | wrangler: 4.6.0(@cloudflare/workers-types@4.20250327.0) 1370 | zod: 3.24.2 1371 | transitivePeerDependencies: 1372 | - '@cloudflare/workers-types' 1373 | - bufferutil 1374 | - utf-8-validate 1375 | 1376 | '@cloudflare/workerd-darwin-64@1.20250321.0': 1377 | optional: true 1378 | 1379 | '@cloudflare/workerd-darwin-arm64@1.20250321.0': 1380 | optional: true 1381 | 1382 | '@cloudflare/workerd-linux-64@1.20250321.0': 1383 | optional: true 1384 | 1385 | '@cloudflare/workerd-linux-arm64@1.20250321.0': 1386 | optional: true 1387 | 1388 | '@cloudflare/workerd-windows-64@1.20250321.0': 1389 | optional: true 1390 | 1391 | '@cloudflare/workers-types@4.20250327.0': {} 1392 | 1393 | '@cspotcode/source-map-support@0.8.1': 1394 | dependencies: 1395 | '@jridgewell/trace-mapping': 0.3.9 1396 | 1397 | '@emnapi/runtime@1.3.1': 1398 | dependencies: 1399 | tslib: 2.8.1 1400 | optional: true 1401 | 1402 | '@esbuild/aix-ppc64@0.24.2': 1403 | optional: true 1404 | 1405 | '@esbuild/aix-ppc64@0.25.2': 1406 | optional: true 1407 | 1408 | '@esbuild/android-arm64@0.24.2': 1409 | optional: true 1410 | 1411 | '@esbuild/android-arm64@0.25.2': 1412 | optional: true 1413 | 1414 | '@esbuild/android-arm@0.24.2': 1415 | optional: true 1416 | 1417 | '@esbuild/android-arm@0.25.2': 1418 | optional: true 1419 | 1420 | '@esbuild/android-x64@0.24.2': 1421 | optional: true 1422 | 1423 | '@esbuild/android-x64@0.25.2': 1424 | optional: true 1425 | 1426 | '@esbuild/darwin-arm64@0.24.2': 1427 | optional: true 1428 | 1429 | '@esbuild/darwin-arm64@0.25.2': 1430 | optional: true 1431 | 1432 | '@esbuild/darwin-x64@0.24.2': 1433 | optional: true 1434 | 1435 | '@esbuild/darwin-x64@0.25.2': 1436 | optional: true 1437 | 1438 | '@esbuild/freebsd-arm64@0.24.2': 1439 | optional: true 1440 | 1441 | '@esbuild/freebsd-arm64@0.25.2': 1442 | optional: true 1443 | 1444 | '@esbuild/freebsd-x64@0.24.2': 1445 | optional: true 1446 | 1447 | '@esbuild/freebsd-x64@0.25.2': 1448 | optional: true 1449 | 1450 | '@esbuild/linux-arm64@0.24.2': 1451 | optional: true 1452 | 1453 | '@esbuild/linux-arm64@0.25.2': 1454 | optional: true 1455 | 1456 | '@esbuild/linux-arm@0.24.2': 1457 | optional: true 1458 | 1459 | '@esbuild/linux-arm@0.25.2': 1460 | optional: true 1461 | 1462 | '@esbuild/linux-ia32@0.24.2': 1463 | optional: true 1464 | 1465 | '@esbuild/linux-ia32@0.25.2': 1466 | optional: true 1467 | 1468 | '@esbuild/linux-loong64@0.24.2': 1469 | optional: true 1470 | 1471 | '@esbuild/linux-loong64@0.25.2': 1472 | optional: true 1473 | 1474 | '@esbuild/linux-mips64el@0.24.2': 1475 | optional: true 1476 | 1477 | '@esbuild/linux-mips64el@0.25.2': 1478 | optional: true 1479 | 1480 | '@esbuild/linux-ppc64@0.24.2': 1481 | optional: true 1482 | 1483 | '@esbuild/linux-ppc64@0.25.2': 1484 | optional: true 1485 | 1486 | '@esbuild/linux-riscv64@0.24.2': 1487 | optional: true 1488 | 1489 | '@esbuild/linux-riscv64@0.25.2': 1490 | optional: true 1491 | 1492 | '@esbuild/linux-s390x@0.24.2': 1493 | optional: true 1494 | 1495 | '@esbuild/linux-s390x@0.25.2': 1496 | optional: true 1497 | 1498 | '@esbuild/linux-x64@0.24.2': 1499 | optional: true 1500 | 1501 | '@esbuild/linux-x64@0.25.2': 1502 | optional: true 1503 | 1504 | '@esbuild/netbsd-arm64@0.24.2': 1505 | optional: true 1506 | 1507 | '@esbuild/netbsd-arm64@0.25.2': 1508 | optional: true 1509 | 1510 | '@esbuild/netbsd-x64@0.24.2': 1511 | optional: true 1512 | 1513 | '@esbuild/netbsd-x64@0.25.2': 1514 | optional: true 1515 | 1516 | '@esbuild/openbsd-arm64@0.24.2': 1517 | optional: true 1518 | 1519 | '@esbuild/openbsd-arm64@0.25.2': 1520 | optional: true 1521 | 1522 | '@esbuild/openbsd-x64@0.24.2': 1523 | optional: true 1524 | 1525 | '@esbuild/openbsd-x64@0.25.2': 1526 | optional: true 1527 | 1528 | '@esbuild/sunos-x64@0.24.2': 1529 | optional: true 1530 | 1531 | '@esbuild/sunos-x64@0.25.2': 1532 | optional: true 1533 | 1534 | '@esbuild/win32-arm64@0.24.2': 1535 | optional: true 1536 | 1537 | '@esbuild/win32-arm64@0.25.2': 1538 | optional: true 1539 | 1540 | '@esbuild/win32-ia32@0.24.2': 1541 | optional: true 1542 | 1543 | '@esbuild/win32-ia32@0.25.2': 1544 | optional: true 1545 | 1546 | '@esbuild/win32-x64@0.24.2': 1547 | optional: true 1548 | 1549 | '@esbuild/win32-x64@0.25.2': 1550 | optional: true 1551 | 1552 | '@fastify/busboy@2.1.1': {} 1553 | 1554 | '@img/sharp-darwin-arm64@0.33.5': 1555 | optionalDependencies: 1556 | '@img/sharp-libvips-darwin-arm64': 1.0.4 1557 | optional: true 1558 | 1559 | '@img/sharp-darwin-x64@0.33.5': 1560 | optionalDependencies: 1561 | '@img/sharp-libvips-darwin-x64': 1.0.4 1562 | optional: true 1563 | 1564 | '@img/sharp-libvips-darwin-arm64@1.0.4': 1565 | optional: true 1566 | 1567 | '@img/sharp-libvips-darwin-x64@1.0.4': 1568 | optional: true 1569 | 1570 | '@img/sharp-libvips-linux-arm64@1.0.4': 1571 | optional: true 1572 | 1573 | '@img/sharp-libvips-linux-arm@1.0.5': 1574 | optional: true 1575 | 1576 | '@img/sharp-libvips-linux-s390x@1.0.4': 1577 | optional: true 1578 | 1579 | '@img/sharp-libvips-linux-x64@1.0.4': 1580 | optional: true 1581 | 1582 | '@img/sharp-libvips-linuxmusl-arm64@1.0.4': 1583 | optional: true 1584 | 1585 | '@img/sharp-libvips-linuxmusl-x64@1.0.4': 1586 | optional: true 1587 | 1588 | '@img/sharp-linux-arm64@0.33.5': 1589 | optionalDependencies: 1590 | '@img/sharp-libvips-linux-arm64': 1.0.4 1591 | optional: true 1592 | 1593 | '@img/sharp-linux-arm@0.33.5': 1594 | optionalDependencies: 1595 | '@img/sharp-libvips-linux-arm': 1.0.5 1596 | optional: true 1597 | 1598 | '@img/sharp-linux-s390x@0.33.5': 1599 | optionalDependencies: 1600 | '@img/sharp-libvips-linux-s390x': 1.0.4 1601 | optional: true 1602 | 1603 | '@img/sharp-linux-x64@0.33.5': 1604 | optionalDependencies: 1605 | '@img/sharp-libvips-linux-x64': 1.0.4 1606 | optional: true 1607 | 1608 | '@img/sharp-linuxmusl-arm64@0.33.5': 1609 | optionalDependencies: 1610 | '@img/sharp-libvips-linuxmusl-arm64': 1.0.4 1611 | optional: true 1612 | 1613 | '@img/sharp-linuxmusl-x64@0.33.5': 1614 | optionalDependencies: 1615 | '@img/sharp-libvips-linuxmusl-x64': 1.0.4 1616 | optional: true 1617 | 1618 | '@img/sharp-wasm32@0.33.5': 1619 | dependencies: 1620 | '@emnapi/runtime': 1.3.1 1621 | optional: true 1622 | 1623 | '@img/sharp-win32-ia32@0.33.5': 1624 | optional: true 1625 | 1626 | '@img/sharp-win32-x64@0.33.5': 1627 | optional: true 1628 | 1629 | '@jridgewell/resolve-uri@3.1.2': {} 1630 | 1631 | '@jridgewell/sourcemap-codec@1.5.0': {} 1632 | 1633 | '@jridgewell/trace-mapping@0.3.9': 1634 | dependencies: 1635 | '@jridgewell/resolve-uri': 3.1.2 1636 | '@jridgewell/sourcemap-codec': 1.5.0 1637 | 1638 | '@rollup/rollup-android-arm-eabi@4.38.0': 1639 | optional: true 1640 | 1641 | '@rollup/rollup-android-arm64@4.38.0': 1642 | optional: true 1643 | 1644 | '@rollup/rollup-darwin-arm64@4.38.0': 1645 | optional: true 1646 | 1647 | '@rollup/rollup-darwin-x64@4.38.0': 1648 | optional: true 1649 | 1650 | '@rollup/rollup-freebsd-arm64@4.38.0': 1651 | optional: true 1652 | 1653 | '@rollup/rollup-freebsd-x64@4.38.0': 1654 | optional: true 1655 | 1656 | '@rollup/rollup-linux-arm-gnueabihf@4.38.0': 1657 | optional: true 1658 | 1659 | '@rollup/rollup-linux-arm-musleabihf@4.38.0': 1660 | optional: true 1661 | 1662 | '@rollup/rollup-linux-arm64-gnu@4.38.0': 1663 | optional: true 1664 | 1665 | '@rollup/rollup-linux-arm64-musl@4.38.0': 1666 | optional: true 1667 | 1668 | '@rollup/rollup-linux-loongarch64-gnu@4.38.0': 1669 | optional: true 1670 | 1671 | '@rollup/rollup-linux-powerpc64le-gnu@4.38.0': 1672 | optional: true 1673 | 1674 | '@rollup/rollup-linux-riscv64-gnu@4.38.0': 1675 | optional: true 1676 | 1677 | '@rollup/rollup-linux-riscv64-musl@4.38.0': 1678 | optional: true 1679 | 1680 | '@rollup/rollup-linux-s390x-gnu@4.38.0': 1681 | optional: true 1682 | 1683 | '@rollup/rollup-linux-x64-gnu@4.38.0': 1684 | optional: true 1685 | 1686 | '@rollup/rollup-linux-x64-musl@4.38.0': 1687 | optional: true 1688 | 1689 | '@rollup/rollup-win32-arm64-msvc@4.38.0': 1690 | optional: true 1691 | 1692 | '@rollup/rollup-win32-ia32-msvc@4.38.0': 1693 | optional: true 1694 | 1695 | '@rollup/rollup-win32-x64-msvc@4.38.0': 1696 | optional: true 1697 | 1698 | '@types/estree@1.0.7': {} 1699 | 1700 | '@types/node@20.14.2': 1701 | dependencies: 1702 | undici-types: 5.26.5 1703 | optional: true 1704 | 1705 | '@vitest/expect@3.0.8': 1706 | dependencies: 1707 | '@vitest/spy': 3.0.8 1708 | '@vitest/utils': 3.0.8 1709 | chai: 5.2.0 1710 | tinyrainbow: 2.0.0 1711 | 1712 | '@vitest/mocker@3.0.8(vite@6.2.3(@types/node@20.14.2))': 1713 | dependencies: 1714 | '@vitest/spy': 3.0.8 1715 | estree-walker: 3.0.3 1716 | magic-string: 0.30.17 1717 | optionalDependencies: 1718 | vite: 6.2.3(@types/node@20.14.2) 1719 | 1720 | '@vitest/pretty-format@3.0.8': 1721 | dependencies: 1722 | tinyrainbow: 2.0.0 1723 | 1724 | '@vitest/pretty-format@3.0.9': 1725 | dependencies: 1726 | tinyrainbow: 2.0.0 1727 | 1728 | '@vitest/runner@3.0.8': 1729 | dependencies: 1730 | '@vitest/utils': 3.0.8 1731 | pathe: 2.0.3 1732 | 1733 | '@vitest/runner@3.0.9': 1734 | dependencies: 1735 | '@vitest/utils': 3.0.9 1736 | pathe: 2.0.3 1737 | 1738 | '@vitest/snapshot@3.0.8': 1739 | dependencies: 1740 | '@vitest/pretty-format': 3.0.8 1741 | magic-string: 0.30.17 1742 | pathe: 2.0.3 1743 | 1744 | '@vitest/snapshot@3.0.9': 1745 | dependencies: 1746 | '@vitest/pretty-format': 3.0.9 1747 | magic-string: 0.30.17 1748 | pathe: 2.0.3 1749 | 1750 | '@vitest/spy@3.0.8': 1751 | dependencies: 1752 | tinyspy: 3.0.2 1753 | 1754 | '@vitest/utils@3.0.8': 1755 | dependencies: 1756 | '@vitest/pretty-format': 3.0.8 1757 | loupe: 3.1.3 1758 | tinyrainbow: 2.0.0 1759 | 1760 | '@vitest/utils@3.0.9': 1761 | dependencies: 1762 | '@vitest/pretty-format': 3.0.9 1763 | loupe: 3.1.3 1764 | tinyrainbow: 2.0.0 1765 | 1766 | acorn-walk@8.3.2: {} 1767 | 1768 | acorn@8.14.0: {} 1769 | 1770 | as-table@1.0.55: 1771 | dependencies: 1772 | printable-characters: 1.0.42 1773 | 1774 | assertion-error@2.0.1: {} 1775 | 1776 | birpc@0.2.14: {} 1777 | 1778 | blake3-wasm@2.1.5: {} 1779 | 1780 | cac@6.7.14: {} 1781 | 1782 | chai@5.2.0: 1783 | dependencies: 1784 | assertion-error: 2.0.1 1785 | check-error: 2.1.1 1786 | deep-eql: 5.0.2 1787 | loupe: 3.1.3 1788 | pathval: 2.0.0 1789 | 1790 | check-error@2.1.1: {} 1791 | 1792 | cjs-module-lexer@1.4.3: {} 1793 | 1794 | color-convert@2.0.1: 1795 | dependencies: 1796 | color-name: 1.1.4 1797 | optional: true 1798 | 1799 | color-name@1.1.4: 1800 | optional: true 1801 | 1802 | color-string@1.9.1: 1803 | dependencies: 1804 | color-name: 1.1.4 1805 | simple-swizzle: 0.2.2 1806 | optional: true 1807 | 1808 | color@4.2.3: 1809 | dependencies: 1810 | color-convert: 2.0.1 1811 | color-string: 1.9.1 1812 | optional: true 1813 | 1814 | cookie@0.5.0: {} 1815 | 1816 | data-uri-to-buffer@2.0.2: {} 1817 | 1818 | debug@4.4.0: 1819 | dependencies: 1820 | ms: 2.1.3 1821 | 1822 | deep-eql@5.0.2: {} 1823 | 1824 | defu@6.1.4: {} 1825 | 1826 | detect-libc@2.0.3: 1827 | optional: true 1828 | 1829 | devalue@4.3.3: {} 1830 | 1831 | es-module-lexer@1.6.0: {} 1832 | 1833 | esbuild@0.24.2: 1834 | optionalDependencies: 1835 | '@esbuild/aix-ppc64': 0.24.2 1836 | '@esbuild/android-arm': 0.24.2 1837 | '@esbuild/android-arm64': 0.24.2 1838 | '@esbuild/android-x64': 0.24.2 1839 | '@esbuild/darwin-arm64': 0.24.2 1840 | '@esbuild/darwin-x64': 0.24.2 1841 | '@esbuild/freebsd-arm64': 0.24.2 1842 | '@esbuild/freebsd-x64': 0.24.2 1843 | '@esbuild/linux-arm': 0.24.2 1844 | '@esbuild/linux-arm64': 0.24.2 1845 | '@esbuild/linux-ia32': 0.24.2 1846 | '@esbuild/linux-loong64': 0.24.2 1847 | '@esbuild/linux-mips64el': 0.24.2 1848 | '@esbuild/linux-ppc64': 0.24.2 1849 | '@esbuild/linux-riscv64': 0.24.2 1850 | '@esbuild/linux-s390x': 0.24.2 1851 | '@esbuild/linux-x64': 0.24.2 1852 | '@esbuild/netbsd-arm64': 0.24.2 1853 | '@esbuild/netbsd-x64': 0.24.2 1854 | '@esbuild/openbsd-arm64': 0.24.2 1855 | '@esbuild/openbsd-x64': 0.24.2 1856 | '@esbuild/sunos-x64': 0.24.2 1857 | '@esbuild/win32-arm64': 0.24.2 1858 | '@esbuild/win32-ia32': 0.24.2 1859 | '@esbuild/win32-x64': 0.24.2 1860 | 1861 | esbuild@0.25.2: 1862 | optionalDependencies: 1863 | '@esbuild/aix-ppc64': 0.25.2 1864 | '@esbuild/android-arm': 0.25.2 1865 | '@esbuild/android-arm64': 0.25.2 1866 | '@esbuild/android-x64': 0.25.2 1867 | '@esbuild/darwin-arm64': 0.25.2 1868 | '@esbuild/darwin-x64': 0.25.2 1869 | '@esbuild/freebsd-arm64': 0.25.2 1870 | '@esbuild/freebsd-x64': 0.25.2 1871 | '@esbuild/linux-arm': 0.25.2 1872 | '@esbuild/linux-arm64': 0.25.2 1873 | '@esbuild/linux-ia32': 0.25.2 1874 | '@esbuild/linux-loong64': 0.25.2 1875 | '@esbuild/linux-mips64el': 0.25.2 1876 | '@esbuild/linux-ppc64': 0.25.2 1877 | '@esbuild/linux-riscv64': 0.25.2 1878 | '@esbuild/linux-s390x': 0.25.2 1879 | '@esbuild/linux-x64': 0.25.2 1880 | '@esbuild/netbsd-arm64': 0.25.2 1881 | '@esbuild/netbsd-x64': 0.25.2 1882 | '@esbuild/openbsd-arm64': 0.25.2 1883 | '@esbuild/openbsd-x64': 0.25.2 1884 | '@esbuild/sunos-x64': 0.25.2 1885 | '@esbuild/win32-arm64': 0.25.2 1886 | '@esbuild/win32-ia32': 0.25.2 1887 | '@esbuild/win32-x64': 0.25.2 1888 | 1889 | estree-walker@3.0.3: 1890 | dependencies: 1891 | '@types/estree': 1.0.7 1892 | 1893 | exit-hook@2.2.1: {} 1894 | 1895 | expect-type@1.2.0: {} 1896 | 1897 | exsolve@1.0.4: {} 1898 | 1899 | fsevents@2.3.3: 1900 | optional: true 1901 | 1902 | get-source@2.0.12: 1903 | dependencies: 1904 | data-uri-to-buffer: 2.0.2 1905 | source-map: 0.6.1 1906 | 1907 | glob-to-regexp@0.4.1: {} 1908 | 1909 | hono@4.7.5: {} 1910 | 1911 | is-arrayish@0.3.2: 1912 | optional: true 1913 | 1914 | itty-router@5.0.18: {} 1915 | 1916 | jose@6.0.10: {} 1917 | 1918 | loupe@3.1.3: {} 1919 | 1920 | magic-string@0.30.17: 1921 | dependencies: 1922 | '@jridgewell/sourcemap-codec': 1.5.0 1923 | 1924 | mime@3.0.0: {} 1925 | 1926 | miniflare@4.20250321.1: 1927 | dependencies: 1928 | '@cspotcode/source-map-support': 0.8.1 1929 | acorn: 8.14.0 1930 | acorn-walk: 8.3.2 1931 | exit-hook: 2.2.1 1932 | glob-to-regexp: 0.4.1 1933 | stoppable: 1.1.0 1934 | undici: 5.29.0 1935 | workerd: 1.20250321.0 1936 | ws: 8.18.0 1937 | youch: 3.2.3 1938 | zod: 3.22.3 1939 | transitivePeerDependencies: 1940 | - bufferutil 1941 | - utf-8-validate 1942 | 1943 | ms@2.1.3: {} 1944 | 1945 | mustache@4.2.0: {} 1946 | 1947 | nanoid@3.3.8: {} 1948 | 1949 | ohash@2.0.11: {} 1950 | 1951 | path-to-regexp@6.3.0: {} 1952 | 1953 | pathe@2.0.3: {} 1954 | 1955 | pathval@2.0.0: {} 1956 | 1957 | picocolors@1.1.1: {} 1958 | 1959 | postcss@8.5.3: 1960 | dependencies: 1961 | nanoid: 3.3.8 1962 | picocolors: 1.1.1 1963 | source-map-js: 1.2.1 1964 | 1965 | printable-characters@1.0.42: {} 1966 | 1967 | rollup@4.38.0: 1968 | dependencies: 1969 | '@types/estree': 1.0.7 1970 | optionalDependencies: 1971 | '@rollup/rollup-android-arm-eabi': 4.38.0 1972 | '@rollup/rollup-android-arm64': 4.38.0 1973 | '@rollup/rollup-darwin-arm64': 4.38.0 1974 | '@rollup/rollup-darwin-x64': 4.38.0 1975 | '@rollup/rollup-freebsd-arm64': 4.38.0 1976 | '@rollup/rollup-freebsd-x64': 4.38.0 1977 | '@rollup/rollup-linux-arm-gnueabihf': 4.38.0 1978 | '@rollup/rollup-linux-arm-musleabihf': 4.38.0 1979 | '@rollup/rollup-linux-arm64-gnu': 4.38.0 1980 | '@rollup/rollup-linux-arm64-musl': 4.38.0 1981 | '@rollup/rollup-linux-loongarch64-gnu': 4.38.0 1982 | '@rollup/rollup-linux-powerpc64le-gnu': 4.38.0 1983 | '@rollup/rollup-linux-riscv64-gnu': 4.38.0 1984 | '@rollup/rollup-linux-riscv64-musl': 4.38.0 1985 | '@rollup/rollup-linux-s390x-gnu': 4.38.0 1986 | '@rollup/rollup-linux-x64-gnu': 4.38.0 1987 | '@rollup/rollup-linux-x64-musl': 4.38.0 1988 | '@rollup/rollup-win32-arm64-msvc': 4.38.0 1989 | '@rollup/rollup-win32-ia32-msvc': 4.38.0 1990 | '@rollup/rollup-win32-x64-msvc': 4.38.0 1991 | fsevents: 2.3.3 1992 | 1993 | semver@7.7.1: {} 1994 | 1995 | sharp@0.33.5: 1996 | dependencies: 1997 | color: 4.2.3 1998 | detect-libc: 2.0.3 1999 | semver: 7.7.1 2000 | optionalDependencies: 2001 | '@img/sharp-darwin-arm64': 0.33.5 2002 | '@img/sharp-darwin-x64': 0.33.5 2003 | '@img/sharp-libvips-darwin-arm64': 1.0.4 2004 | '@img/sharp-libvips-darwin-x64': 1.0.4 2005 | '@img/sharp-libvips-linux-arm': 1.0.5 2006 | '@img/sharp-libvips-linux-arm64': 1.0.4 2007 | '@img/sharp-libvips-linux-s390x': 1.0.4 2008 | '@img/sharp-libvips-linux-x64': 1.0.4 2009 | '@img/sharp-libvips-linuxmusl-arm64': 1.0.4 2010 | '@img/sharp-libvips-linuxmusl-x64': 1.0.4 2011 | '@img/sharp-linux-arm': 0.33.5 2012 | '@img/sharp-linux-arm64': 0.33.5 2013 | '@img/sharp-linux-s390x': 0.33.5 2014 | '@img/sharp-linux-x64': 0.33.5 2015 | '@img/sharp-linuxmusl-arm64': 0.33.5 2016 | '@img/sharp-linuxmusl-x64': 0.33.5 2017 | '@img/sharp-wasm32': 0.33.5 2018 | '@img/sharp-win32-ia32': 0.33.5 2019 | '@img/sharp-win32-x64': 0.33.5 2020 | optional: true 2021 | 2022 | short-unique-id@5.2.2: {} 2023 | 2024 | siginfo@2.0.0: {} 2025 | 2026 | simple-swizzle@0.2.2: 2027 | dependencies: 2028 | is-arrayish: 0.3.2 2029 | optional: true 2030 | 2031 | source-map-js@1.2.1: {} 2032 | 2033 | source-map@0.6.1: {} 2034 | 2035 | stackback@0.0.2: {} 2036 | 2037 | stacktracey@2.1.8: 2038 | dependencies: 2039 | as-table: 1.0.55 2040 | get-source: 2.0.12 2041 | 2042 | std-env@3.8.1: {} 2043 | 2044 | stoppable@1.1.0: {} 2045 | 2046 | tinybench@2.9.0: {} 2047 | 2048 | tinyexec@0.3.2: {} 2049 | 2050 | tinypool@1.0.2: {} 2051 | 2052 | tinyrainbow@2.0.0: {} 2053 | 2054 | tinyspy@3.0.2: {} 2055 | 2056 | tslib@2.8.1: 2057 | optional: true 2058 | 2059 | typescript@5.8.2: {} 2060 | 2061 | ufo@1.5.4: {} 2062 | 2063 | undici-types@5.26.5: 2064 | optional: true 2065 | 2066 | undici@5.29.0: 2067 | dependencies: 2068 | '@fastify/busboy': 2.1.1 2069 | 2070 | unenv@2.0.0-rc.15: 2071 | dependencies: 2072 | defu: 6.1.4 2073 | exsolve: 1.0.4 2074 | ohash: 2.0.11 2075 | pathe: 2.0.3 2076 | ufo: 1.5.4 2077 | 2078 | vite-node@3.0.8(@types/node@20.14.2): 2079 | dependencies: 2080 | cac: 6.7.14 2081 | debug: 4.4.0 2082 | es-module-lexer: 1.6.0 2083 | pathe: 2.0.3 2084 | vite: 6.2.3(@types/node@20.14.2) 2085 | transitivePeerDependencies: 2086 | - '@types/node' 2087 | - jiti 2088 | - less 2089 | - lightningcss 2090 | - sass 2091 | - sass-embedded 2092 | - stylus 2093 | - sugarss 2094 | - supports-color 2095 | - terser 2096 | - tsx 2097 | - yaml 2098 | 2099 | vite@6.2.3(@types/node@20.14.2): 2100 | dependencies: 2101 | esbuild: 0.25.2 2102 | postcss: 8.5.3 2103 | rollup: 4.38.0 2104 | optionalDependencies: 2105 | '@types/node': 20.14.2 2106 | fsevents: 2.3.3 2107 | 2108 | vitest@3.0.8(@types/node@20.14.2): 2109 | dependencies: 2110 | '@vitest/expect': 3.0.8 2111 | '@vitest/mocker': 3.0.8(vite@6.2.3(@types/node@20.14.2)) 2112 | '@vitest/pretty-format': 3.0.9 2113 | '@vitest/runner': 3.0.8 2114 | '@vitest/snapshot': 3.0.8 2115 | '@vitest/spy': 3.0.8 2116 | '@vitest/utils': 3.0.8 2117 | chai: 5.2.0 2118 | debug: 4.4.0 2119 | expect-type: 1.2.0 2120 | magic-string: 0.30.17 2121 | pathe: 2.0.3 2122 | std-env: 3.8.1 2123 | tinybench: 2.9.0 2124 | tinyexec: 0.3.2 2125 | tinypool: 1.0.2 2126 | tinyrainbow: 2.0.0 2127 | vite: 6.2.3(@types/node@20.14.2) 2128 | vite-node: 3.0.8(@types/node@20.14.2) 2129 | why-is-node-running: 2.3.0 2130 | optionalDependencies: 2131 | '@types/node': 20.14.2 2132 | transitivePeerDependencies: 2133 | - jiti 2134 | - less 2135 | - lightningcss 2136 | - msw 2137 | - sass 2138 | - sass-embedded 2139 | - stylus 2140 | - sugarss 2141 | - supports-color 2142 | - terser 2143 | - tsx 2144 | - yaml 2145 | 2146 | why-is-node-running@2.3.0: 2147 | dependencies: 2148 | siginfo: 2.0.0 2149 | stackback: 0.0.2 2150 | 2151 | workerd@1.20250321.0: 2152 | optionalDependencies: 2153 | '@cloudflare/workerd-darwin-64': 1.20250321.0 2154 | '@cloudflare/workerd-darwin-arm64': 1.20250321.0 2155 | '@cloudflare/workerd-linux-64': 1.20250321.0 2156 | '@cloudflare/workerd-linux-arm64': 1.20250321.0 2157 | '@cloudflare/workerd-windows-64': 1.20250321.0 2158 | 2159 | wrangler@4.6.0(@cloudflare/workers-types@4.20250327.0): 2160 | dependencies: 2161 | '@cloudflare/kv-asset-handler': 0.4.0 2162 | '@cloudflare/unenv-preset': 2.3.1(unenv@2.0.0-rc.15)(workerd@1.20250321.0) 2163 | blake3-wasm: 2.1.5 2164 | esbuild: 0.24.2 2165 | miniflare: 4.20250321.1 2166 | path-to-regexp: 6.3.0 2167 | unenv: 2.0.0-rc.15 2168 | workerd: 1.20250321.0 2169 | optionalDependencies: 2170 | '@cloudflare/workers-types': 4.20250327.0 2171 | fsevents: 2.3.3 2172 | sharp: 0.33.5 2173 | transitivePeerDependencies: 2174 | - bufferutil 2175 | - utf-8-validate 2176 | 2177 | ws@8.18.0: {} 2178 | 2179 | youch@3.2.3: 2180 | dependencies: 2181 | cookie: 0.5.0 2182 | mustache: 4.2.0 2183 | stacktracey: 2.1.8 2184 | 2185 | zod@3.22.3: {} 2186 | 2187 | zod@3.24.2: {} 2188 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - workers/* 3 | - packages/* 4 | 5 | catalog: 6 | "wrangler": "^4.0.0" 7 | "@cloudflare/vitest-pool-workers": "^0.8.0" 8 | "@cloudflare/workers-types": "^4.20250313.0" 9 | "vitest": "3.0.8" 10 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "skipLibCheck": true, 4 | "module": "esnext", 5 | "target": "esnext", 6 | "lib": ["esnext"], 7 | "sourceMap": true, 8 | "moduleResolution": "node", 9 | "esModuleInterop": true, 10 | "types": [ 11 | "@cloudflare/workers-types", 12 | "@cloudflare/workers-types/experimental", 13 | "@cloudflare/vitest-pool-workers" 14 | ] 15 | }, 16 | "exclude": ["node_modules"] 17 | } 18 | -------------------------------------------------------------------------------- /vitest.workspace.js: -------------------------------------------------------------------------------- 1 | import { defineWorkspace } from "vitest/config"; 2 | 3 | // defineWorkspace provides a nice type hinting DX 4 | export default defineWorkspace(["workers/*", "packages/*"]); 5 | -------------------------------------------------------------------------------- /workers/apigw/README.md: -------------------------------------------------------------------------------- 1 | # `apigw` 🌉 2 | 3 | API Gateway (`apigw`) is a worker designed to "shim" between different forms of HTTP Authentication. 4 | 5 | For example, some applications only support using HTTP Basic Auth (ie, terraform http backend) but the `tfstate` worker 6 | is 7 | protected by Cloudflare Access. 8 | So, in order to support terraform tooling, the API GW will inject the appropriate `CF-Access-Client-Id` 9 | and `CF-Access-Client-Secret`headers, based on the incoming `Authorization` header. The altered request will then be 10 | sent upstream. 11 | -------------------------------------------------------------------------------- /workers/apigw/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "apigw", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "wrangler dev", 7 | "deploy": "wrangler deploy", 8 | "test": "vitest" 9 | }, 10 | "devDependencies": { 11 | "wrangler": "catalog:", 12 | "@cloudflare/vitest-pool-workers": "catalog:", 13 | "@cloudflare/workers-types": "catalog:", 14 | "vitest": "catalog:" 15 | }, 16 | "dependencies": { 17 | "itty-router": "^5.0.18" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /workers/apigw/src/index.ts: -------------------------------------------------------------------------------- 1 | import { Router } from "itty-router"; 2 | 3 | const router = Router(); 4 | 5 | router.all("/tfstate/*", async (request: Request) => { 6 | const authorization: string | undefined = 7 | request.headers.get("authorization"); 8 | if (!authorization) return fetch(request); 9 | 10 | const [authScheme, token] = authorization.split(" "); 11 | if (authScheme.toLowerCase() !== "basic") 12 | return new Response( 13 | `Authentication type ${authScheme} is not supported for this endpoint.`, 14 | { status: 401 }, 15 | ); 16 | 17 | const [user, pass] = atob(token).split(":"); 18 | const url = new URL(request.url); 19 | url.hostname = "tfstate.mirio.dev"; 20 | url.pathname = url.pathname.slice("/tfstate".length); 21 | 22 | const req = new Request(url.toString(), request); 23 | const headers = req.headers; 24 | headers.set("Cf-Access-Client-Id", user); 25 | headers.set("Cf-Access-Client-Secret", pass); 26 | 27 | return fetch(req); 28 | }); 29 | 30 | router.all("/clickhouse/*", async (request: Request) => { 31 | const authorization: string | undefined = 32 | request.headers.get("authorization"); 33 | if (!authorization) return fetch(request); 34 | 35 | const [authScheme, token] = authorization.split(" "); 36 | if (authScheme.toLowerCase() !== "basic") 37 | return new Response( 38 | `Authentication type ${authScheme} is not supported for this endpoint.`, 39 | { status: 401 }, 40 | ); 41 | 42 | const [user, pass] = atob(token).split(":"); 43 | const url = new URL(request.url); 44 | url.hostname = "clickhouse.mirio.dev"; 45 | url.pathname = url.pathname.slice("/clickhouse".length); 46 | 47 | const req = new Request(url.toString(), request); 48 | const headers = req.headers; 49 | headers.set("Cf-Access-Client-Id", user); 50 | headers.set("Cf-Access-Client-Secret", pass); 51 | headers.delete("authorization"); 52 | 53 | return fetch(req); 54 | }); 55 | 56 | router.all("/echo/*", async (request: Request) => { 57 | const url = new URL(request.url); 58 | url.host = "echo.mirio.dev"; 59 | url.pathname = url.pathname.slice("/echo".length); 60 | return fetch(url.toString(), request); 61 | }); 62 | 63 | router.all("*", () => new Response("Go away.", { status: 404 })); 64 | 65 | export default router; 66 | -------------------------------------------------------------------------------- /workers/apigw/test/env.d.ts: -------------------------------------------------------------------------------- 1 | import type { Env } from "../src/types/env"; 2 | 3 | declare module "cloudflare:test" { 4 | // Controls the type of `import("cloudflare:test").env` 5 | interface ProvidedEnv extends Env {} 6 | } 7 | -------------------------------------------------------------------------------- /workers/apigw/test/index.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | createExecutionContext, 3 | env, 4 | waitOnExecutionContext, 5 | } from "cloudflare:test"; 6 | import { describe, expect, it } from "vitest"; 7 | // Could import any other source file/function here 8 | import worker from "../src"; 9 | 10 | describe("Echo worker", () => { 11 | it("responds with 200 OK", async () => { 12 | const request = new Request("http://example.com/echo"); 13 | // Create an empty context to pass to `worker.fetch()` 14 | const ctx = createExecutionContext(); 15 | const response = await worker.fetch(request, env, ctx); 16 | // Wait for all `Promise`s passed to `ctx.waitUntil()` to settle before running test assertions 17 | await waitOnExecutionContext(ctx); 18 | expect(response.status).toBe(200); 19 | console.log(await response.text()); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /workers/apigw/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json" 3 | } 4 | -------------------------------------------------------------------------------- /workers/apigw/vitest.config.js: -------------------------------------------------------------------------------- 1 | import { defineWorkersConfig } from "@cloudflare/vitest-pool-workers/config"; 2 | 3 | export default defineWorkersConfig({ 4 | test: { 5 | poolOptions: { 6 | workers: { 7 | wrangler: { configPath: "./wrangler.toml" }, 8 | }, 9 | }, 10 | }, 11 | }); 12 | -------------------------------------------------------------------------------- /workers/apigw/wrangler.toml: -------------------------------------------------------------------------------- 1 | name = "apigw" 2 | account_id = "f5686db3c4f5b3e38b8f15b0561a28a8" 3 | compatibility_date = "2024-03-26" 4 | compatibility_flags = ["nodejs_compat"] 5 | 6 | main = "src/index.ts" 7 | 8 | routes = [ 9 | { pattern = "apigw.eragon.xyz", custom_domain = true }, # deploying on a separate zone to allow using CF Access on upstream 10 | ] 11 | -------------------------------------------------------------------------------- /workers/echo/README.md: -------------------------------------------------------------------------------- 1 | # `echo` 📣 2 | 3 | Echo back the request received 4 | 5 | ``` 6 | curl https://echo.mirio.dev | jq 7 | 8 | { 9 | "headers": { 10 | "accept": "*/*", 11 | "accept-encoding": "gzip", 12 | "cf-connecting-ip": "63.229.18.140", 13 | "cf-ipcountry": "US", 14 | "cf-ray": "7401e028faf53089", 15 | "cf-visitor": "{\"scheme\":\"https\"}", 16 | "connection": "Keep-Alive", 17 | "host": "echo.mirio.dev", 18 | "user-agent": "curl/7.79.1", 19 | "x-forwarded-proto": "https", 20 | "x-real-ip": "63.229.18.140" 21 | }, 22 | "url": "https://echo.mirio.dev/", 23 | "method": "GET", 24 | "body": null 25 | } 26 | ``` 27 | -------------------------------------------------------------------------------- /workers/echo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "echo", 3 | "version": "0.0.0", 4 | "private": true, 5 | "type": "module", 6 | "scripts": { 7 | "dev": "wrangler dev", 8 | "deploy": "wrangler deploy", 9 | "test": "vitest" 10 | }, 11 | "devDependencies": { 12 | "wrangler": "catalog:", 13 | "@cloudflare/vitest-pool-workers": "catalog:", 14 | "@cloudflare/workers-types": "catalog:", 15 | "vitest": "catalog:" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /workers/echo/src/index.ts: -------------------------------------------------------------------------------- 1 | export interface ResponseBody { 2 | headers: Record; 3 | body: unknown; 4 | url: string; 5 | method: string; 6 | query: Record; 7 | } 8 | 9 | export default { 10 | async fetch(request: Request): Promise { 11 | const { url, method, headers } = request; 12 | const parsedUrl = new URL(url); 13 | 14 | const body = request.body ? await request.json() : null; 15 | const result = { 16 | headers: Object.fromEntries(headers), 17 | body, 18 | url, 19 | method, 20 | query: Object.fromEntries(parsedUrl.searchParams), 21 | }; 22 | const responseBody = parsedUrl.searchParams.get("pretty") 23 | ? JSON.stringify(result, null, 2) 24 | : JSON.stringify(result); 25 | 26 | return new Response(responseBody, { 27 | headers: { 28 | "content-type": "application/json", 29 | }, 30 | }); 31 | }, 32 | }; 33 | -------------------------------------------------------------------------------- /workers/echo/test/index.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest"; 2 | // Could import any other source file/function here 3 | import worker, { type ResponseBody } from "../src"; 4 | 5 | describe("Echo worker", () => { 6 | it("get responds with 200 OK", async () => { 7 | const request = new Request("http://example.com?pretty=true"); 8 | const response = await worker.fetch(request); 9 | expect(response.status).toBe(200); 10 | const responseBody = (await response.json()) as ResponseBody; 11 | expect(responseBody).toBeDefined(); 12 | expect(responseBody.headers).toBeDefined(); 13 | expect(responseBody.body).toBeDefined(); 14 | expect(responseBody.body).toBeNull(); 15 | expect(responseBody.url).toBe("http://example.com?pretty=true"); 16 | expect(responseBody.method).toBe("GET"); 17 | expect(responseBody.query).toEqual({ pretty: "true" }); 18 | }); 19 | 20 | it("post responds with 200 OK and body", async () => { 21 | const request = new Request("http://example.com", { 22 | method: "POST", 23 | body: JSON.stringify({ hello: "world" }), 24 | }); 25 | const response = await worker.fetch(request); 26 | expect(response.status).toBe(200); 27 | const responseBody = (await response.json()) as ResponseBody; 28 | expect(responseBody).toBeDefined(); 29 | expect(responseBody.headers).toBeDefined(); 30 | expect(responseBody.body).toBeDefined(); 31 | expect(responseBody.body).toStrictEqual({ hello: "world" }); 32 | expect(responseBody.url).toBe("http://example.com"); 33 | expect(responseBody.method).toBe("POST"); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /workers/echo/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json" 3 | } 4 | -------------------------------------------------------------------------------- /workers/echo/vitest.config.js: -------------------------------------------------------------------------------- 1 | import { defineWorkersConfig } from "@cloudflare/vitest-pool-workers/config"; 2 | 3 | export default defineWorkersConfig({ 4 | test: { 5 | poolOptions: { 6 | workers: { 7 | wrangler: { configPath: "./wrangler.toml" }, 8 | }, 9 | }, 10 | }, 11 | }); 12 | -------------------------------------------------------------------------------- /workers/echo/wrangler.toml: -------------------------------------------------------------------------------- 1 | name = "echo" 2 | account_id = "f5686db3c4f5b3e38b8f15b0561a28a8" 3 | compatibility_date = "2024-03-26" 4 | compatibility_flags = ["nodejs_compat"] 5 | 6 | main = "src/index.ts" 7 | 8 | routes = [{ pattern = "echo.mirio.dev", custom_domain = true }] 9 | -------------------------------------------------------------------------------- /workers/hash/README.md: -------------------------------------------------------------------------------- 1 | # `hash` #️⃣ 2 | 3 | POST data and get the `sha256`, `md5` or other supported hash back! Data is returned as the hex encoding of the final digest. 4 | 5 | **Note:** When using `curl`, be sure to use `--data-binary` to prevent `curl` from transforming the data before sending it. 6 | 7 | ``` 8 | curl -XPOST https://hash.mirio.dev/md5 --data-binary @data.bin 9 | c27830b0f2af9af174da9b25e56be6ff 10 | 11 | curl -XPOST https://hash.mirio.dev/sha256 --data-binary @data.bin 12 | d457d2a4c60670d56b6cf5ed36a362d99041f37cc68b91297eecd06de5870301 13 | ``` 14 | 15 | ### Supported algorithms 16 | 17 | - `md5` 18 | - `sha1` 19 | - `sha256` 20 | - `sha384` 21 | - `sha512` 22 | -------------------------------------------------------------------------------- /workers/hash/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hash", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "wrangler dev", 7 | "deploy": "wrangler deploy", 8 | "test": "vitest" 9 | }, 10 | "devDependencies": { 11 | "wrangler": "catalog:", 12 | "@cloudflare/vitest-pool-workers": "catalog:", 13 | "@cloudflare/workers-types": "catalog:", 14 | "vitest": "catalog:" 15 | }, 16 | "dependencies": { 17 | "itty-router": "^5.0.18" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /workers/hash/src/index.ts: -------------------------------------------------------------------------------- 1 | import { type IRequest, Router } from "itty-router"; 2 | 3 | function buf2hex(buffer: ArrayBuffer): string { 4 | return [...new Uint8Array(buffer)] 5 | .map((x) => x.toString(16).padStart(2, "0")) 6 | .join(""); 7 | } 8 | 9 | const router = Router(); 10 | 11 | const algs = { 12 | sha1: "SHA-1", 13 | sha256: "SHA-256", 14 | sha384: "SHA-384", 15 | sha512: "SHA-512", 16 | md5: "MD5", 17 | }; 18 | 19 | router.post("/:alg", async (request: Request) => { 20 | const { params } = request as IRequest; 21 | const alg: string | undefined = params?.alg; 22 | if (!alg || !(alg in algs)) 23 | return new Response( 24 | `Invalid algorithm.\nSupported algorithms are: ${[ 25 | ...new Set(Object.keys(algs)), 26 | ].join(", ")}\n`, 27 | { 28 | status: 400, 29 | }, 30 | ); 31 | return new Response( 32 | `${buf2hex( 33 | await crypto.subtle.digest(algs[alg], await request.arrayBuffer()), 34 | )}\n`, 35 | ); 36 | }); 37 | 38 | router.post( 39 | "*", 40 | async (request: Request) => new Response("Not found.\n", { status: 404 }), 41 | ); 42 | 43 | export default router; 44 | -------------------------------------------------------------------------------- /workers/hash/test/index.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | createExecutionContext, 3 | env, 4 | waitOnExecutionContext, 5 | } from "cloudflare:test"; 6 | import { describe, expect, it } from "vitest"; 7 | // Could import any other source file/function here 8 | import worker from "../src"; 9 | 10 | describe("Echo worker", () => { 11 | it("responds with md5", async () => { 12 | const request = new Request("http://example.com/md5", { 13 | method: "POST", 14 | body: "hello world", 15 | }); 16 | // Create an empty context to pass to `worker.fetch()` 17 | const ctx = createExecutionContext(); 18 | const response = await worker.fetch(request, env, ctx); 19 | // Wait for all `Promise`s passed to `ctx.waitUntil()` to settle before running test assertions 20 | await waitOnExecutionContext(ctx); 21 | expect(response.status).toBe(200); 22 | expect(await response.text()).toBe("5eb63bbbe01eeed093cb22bb8f5acdc3\n"); // echo -n 'hello world' | md5sum 23 | }); 24 | 25 | it("responds with sha1", async () => { 26 | const request = new Request("http://example.com/sha1", { 27 | method: "POST", 28 | body: "hello world", 29 | }); 30 | // Create an empty context to pass to `worker.fetch()` 31 | const ctx = createExecutionContext(); 32 | const response = await worker.fetch(request, env, ctx); 33 | // Wait for all `Promise`s passed to `ctx.waitUntil()` to settle before running test assertions 34 | await waitOnExecutionContext(ctx); 35 | expect(response.status).toBe(200); 36 | expect(await response.text()).toBe( 37 | "2aae6c35c94fcfb415dbe95f408b9ce91ee846ed\n", 38 | ); // echo -n 'hello world' | sha1sum 39 | }); 40 | 41 | it("responds with sha256", async () => { 42 | const request = new Request("http://example.com/sha256", { 43 | method: "POST", 44 | body: "hello world", 45 | }); 46 | // Create an empty context to pass to `worker.fetch()` 47 | const ctx = createExecutionContext(); 48 | const response = await worker.fetch(request, env, ctx); 49 | // Wait for all `Promise`s passed to `ctx.waitUntil()` to settle before running test assertions 50 | await waitOnExecutionContext(ctx); 51 | expect(response.status).toBe(200); 52 | expect(await response.text()).toBe( 53 | "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9\n", 54 | ); // echo -n 'hello world' | sha256sum 55 | }); 56 | 57 | it("responds with sha384", async () => { 58 | const request = new Request("http://example.com/sha384", { 59 | method: "POST", 60 | body: "hello world", 61 | }); 62 | // Create an empty context to pass to `worker.fetch()` 63 | const ctx = createExecutionContext(); 64 | const response = await worker.fetch(request, env, ctx); 65 | // Wait for all `Promise`s passed to `ctx.waitUntil()` to settle before running test assertions 66 | await waitOnExecutionContext(ctx); 67 | expect(response.status).toBe(200); 68 | expect(await response.text()).toBe( 69 | "fdbd8e75a67f29f701a4e040385e2e23986303ea10239211af907fcbb83578b3e417cb71ce646efd0819dd8c088de1bd\n", 70 | ); // echo -n 'hello world' | sha384sum 71 | }); 72 | 73 | it("responds with sha512", async () => { 74 | const request = new Request("http://example.com/sha512", { 75 | method: "POST", 76 | body: "hello world", 77 | }); 78 | // Create an empty context to pass to `worker.fetch()` 79 | const ctx = createExecutionContext(); 80 | const response = await worker.fetch(request, env, ctx); 81 | // Wait for all `Promise`s passed to `ctx.waitUntil()` to settle before running test assertions 82 | await waitOnExecutionContext(ctx); 83 | expect(response.status).toBe(200); 84 | expect(await response.text()).toBe( 85 | "309ecc489c12d6eb4cc40f50c902f2b4d0ed77ee511a7c7a9bcd3ca86d4cd86f989dd35bc5ff499670da34255b45b0cfd830e81f605dcf7dc5542e93ae9cd76f\n", 86 | ); // echo -n 'hello world' | sha512sum 87 | }); 88 | }); 89 | -------------------------------------------------------------------------------- /workers/hash/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json" 3 | } 4 | -------------------------------------------------------------------------------- /workers/hash/vitest.config.js: -------------------------------------------------------------------------------- 1 | import { defineWorkersConfig } from "@cloudflare/vitest-pool-workers/config"; 2 | 3 | export default defineWorkersConfig({ 4 | test: { 5 | poolOptions: { 6 | workers: { 7 | wrangler: { configPath: "./wrangler.toml" }, 8 | }, 9 | }, 10 | }, 11 | }); 12 | -------------------------------------------------------------------------------- /workers/hash/wrangler.toml: -------------------------------------------------------------------------------- 1 | name = "hash" 2 | account_id = "f5686db3c4f5b3e38b8f15b0561a28a8" 3 | compatibility_date = "2024-03-26" 4 | compatibility_flags = ["nodejs_compat"] 5 | 6 | main = "src/index.ts" 7 | 8 | routes = [{ pattern = "hash.mirio.dev", custom_domain = true }] 9 | -------------------------------------------------------------------------------- /workers/hello-world/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hello-world", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "wrangler dev", 7 | "deploy": "wrangler deploy", 8 | "deploy:prod": "wrangler deploy --env production", 9 | "test": "vitest" 10 | }, 11 | "devDependencies": { 12 | "wrangler": "catalog:", 13 | "@cloudflare/vitest-pool-workers": "catalog:", 14 | "@cloudflare/workers-types": "catalog:", 15 | "vitest": "catalog:" 16 | }, 17 | "dependencies": { 18 | "@mirio/analytics": "workspace:" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /workers/hello-world/src/index.ts: -------------------------------------------------------------------------------- 1 | import { HTTPAnalytics } from "@mirio/analytics"; 2 | 3 | interface Env { 4 | HTTP_REQUESTS: AnalyticsEngineDataset; 5 | } 6 | 7 | export default { 8 | async fetch( 9 | request: Request, 10 | env: Env, 11 | _: ExecutionContext, 12 | ): Promise { 13 | // Log the request 14 | if (env?.HTTP_REQUESTS) { 15 | new HTTPAnalytics(env.HTTP_REQUESTS).observe(request); 16 | } 17 | 18 | return new Response("Hello, World!"); 19 | }, 20 | }; 21 | -------------------------------------------------------------------------------- /workers/hello-world/test/index.spec.ts: -------------------------------------------------------------------------------- 1 | import { SELF } from "cloudflare:test"; 2 | 3 | import { describe, expect, it } from "vitest"; 4 | 5 | describe("Echo worker", () => { 6 | it("responds with 200 OK", async () => { 7 | const response = await SELF.fetch("http://example.com/"); 8 | expect(response.status).toBe(200); 9 | expect(await response.text()).toBe("Hello, World!"); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /workers/hello-world/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json" 3 | } 4 | -------------------------------------------------------------------------------- /workers/hello-world/vitest.config.js: -------------------------------------------------------------------------------- 1 | import { defineWorkersConfig } from "@cloudflare/vitest-pool-workers/config"; 2 | 3 | export default defineWorkersConfig({ 4 | test: { 5 | poolOptions: { 6 | workers: { 7 | wrangler: { configPath: "./wrangler.toml" }, 8 | }, 9 | }, 10 | }, 11 | }); 12 | -------------------------------------------------------------------------------- /workers/hello-world/wrangler.toml: -------------------------------------------------------------------------------- 1 | name = "hello-world-dev" 2 | account_id = "f5686db3c4f5b3e38b8f15b0561a28a8" 3 | compatibility_date = "2024-03-26" 4 | compatibility_flags = ["nodejs_compat"] 5 | 6 | main = "src/index.ts" 7 | 8 | routes = [{ pattern = "hello-world-dev.mirio.dev", custom_domain = true }] 9 | 10 | [[analytics_engine_datasets]] 11 | binding = "HTTP_REQUESTS" 12 | dataset = "http_requests" 13 | 14 | [env.production] 15 | name = "hello-world" 16 | routes = [{ pattern = "hello-world.mirio.dev", custom_domain = true }] 17 | 18 | [[env.production.analytics_engine_datasets]] 19 | binding = "HTTP_REQUESTS" 20 | dataset = "http_requests" 21 | -------------------------------------------------------------------------------- /workers/ip/README.md: -------------------------------------------------------------------------------- 1 | # `ip` 🌎 2 | 3 | Get information about your IP Address! 4 | 5 | - `/`: returns the clients IP Address 6 | - `/`: returns the value for the requested property. 7 | 8 | | property | description | example | 9 | | ------------ | ---------------------------------------------------------------------------------------------------------------- | ------------------------ | 10 | | `asn` | ASN of the incoming request. | `209` | 11 | | `aso` | The organization which owns the ASN of the incoming request. | `CenturyLink` | 12 | | `city` | City of the incoming request. | `Seattle` | 13 | | `colo` | The three-letter IATA airport code of the Cloudflare data center that the request hit. | `SEA` | 14 | | `country` | The two-letter country code of the incoming request. | `US` | 15 | | `latlong` | The latitude and longitude of the incoming request, separated by a comma. | `47.57410,-122.39750` | 16 | | `region` | If known, the ISO 3166-2 name for the first level region associated with the IP address of the incoming request. | `Washington` | 17 | | `tlsCipher` | The tls cipher used for the connection to Cloudflare. | `AEAD-AES128-GCM-SHA256` | 18 | | `tlsVersion` | The tls verson used for the connection to Cloudflare. | `TLSv1.3 ` | 19 | | `timezone` | Timezone of the incoming request. | `America/Los_Angeles` | 20 | -------------------------------------------------------------------------------- /workers/ip/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ip", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "wrangler dev", 7 | "deploy": "wrangler deploy", 8 | "test": "vitest" 9 | }, 10 | "devDependencies": { 11 | "wrangler": "catalog:", 12 | "@cloudflare/vitest-pool-workers": "catalog:", 13 | "@cloudflare/workers-types": "catalog:", 14 | "vitest": "catalog:" 15 | }, 16 | "dependencies": { 17 | "itty-router": "^5.0.18" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /workers/ip/src/index.ts: -------------------------------------------------------------------------------- 1 | import { type IRequest, Router } from "itty-router"; 2 | 3 | const getIp = (request: Request) => 4 | request.headers.get("cf-connecting-ip") || ""; 5 | 6 | const propertyDefinitions = { 7 | asn: "Autonomous system number (ASN)", 8 | aso: "Autonomous system organization (ASO)", 9 | colo: "Cloudflare colo", 10 | city: "City", 11 | country: "Country", 12 | latlong: "Latitude and longitude", 13 | region: "Region", 14 | tlsCipher: "TLS cipher", 15 | tlsVersion: "TLS version", 16 | timezone: "Timezone", 17 | }; 18 | 19 | const propertyMapping: { [key: string]: keyof IncomingRequestCfProperties } = { 20 | asn: "asn", 21 | aso: "asOrganization", 22 | colo: "colo", 23 | city: "city", 24 | country: "country", 25 | latlong: "latlong", 26 | region: "region", 27 | tlsCipher: "tlsCipher", 28 | tlsVersion: "tlsVersion", 29 | timezone: "timezone", 30 | }; 31 | 32 | const router = Router(); 33 | 34 | router 35 | .all("*") 36 | .get("/", (request: Request) => { 37 | const ip = getIp(request); 38 | return new Response(`${ip}\n`, { 39 | headers: { 40 | "content-type": "text/plain", 41 | }, 42 | }); 43 | }) 44 | .get("/help", () => { 45 | const properties = Object.entries(propertyDefinitions) 46 | .map(([key, value]) => `\t${key}: ${value}`) 47 | .join("\n"); 48 | return new Response(`Usage: curl https://ip.mirio.dev/:property [-4/-6] 49 | 50 | Properties:\n${properties}\n`); 51 | }) 52 | .get("/json", (request: Request) => { 53 | const { url, cf } = request; 54 | const ip = getIp(request); 55 | const parsedUrl = new URL(url); 56 | const result = { ip, info: cf }; 57 | const responseBody = parsedUrl.searchParams.get("pretty") 58 | ? JSON.stringify(result, null, 2) 59 | : JSON.stringify(result); 60 | 61 | return new Response(responseBody, { 62 | headers: { 63 | "content-type": "application/json; charset=UTF-8", 64 | }, 65 | }); 66 | }) 67 | .get("/:property", (request: IRequest) => { 68 | const { params } = request; 69 | if (!params) return new Response(null, { status: 204 }); 70 | const { cf } = request; 71 | if (!cf) return new Response(null, { status: 204 }); 72 | 73 | const property = propertyMapping[params.property]; 74 | if (!property) return new Response("Not found.\n", { status: 404 }); 75 | const prop = request.cf?.[property] || "unknown"; 76 | if (property === "latlong") { 77 | const lat = request.cf?.latitude || "unknown"; 78 | const long = request.cf?.longitude || "unknown"; 79 | return new Response(`${lat},${long}\n`, { 80 | headers: { 81 | "content-type": "text/csv", 82 | "content-disposition": "inline", 83 | }, 84 | }); 85 | } 86 | return new Response(`${prop}\n`, { 87 | headers: { 88 | "content-type": "text/plain", 89 | }, 90 | }); 91 | }); 92 | 93 | export default router; 94 | -------------------------------------------------------------------------------- /workers/ip/test/index.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | createExecutionContext, 3 | env, 4 | waitOnExecutionContext, 5 | } from "cloudflare:test"; 6 | import { describe, expect, it } from "vitest"; 7 | // Could import any other source file/function here 8 | import worker from "../src"; 9 | 10 | describe("Echo worker", () => { 11 | it("responds with 200 OK", async () => { 12 | const request = new Request("http://example.com/", { 13 | headers: { "cf-connecting-ip": "127.0.0.1" }, 14 | }); 15 | // Create an empty context to pass to `worker.fetch()` 16 | const ctx = createExecutionContext(); 17 | const response = await worker.fetch(request, env, ctx); 18 | // Wait for all `Promise`s passed to `ctx.waitUntil()` to settle before running test assertions 19 | await waitOnExecutionContext(ctx); 20 | expect(response.status).toBe(200); 21 | expect(await response.text()).toBe("127.0.0.1\n"); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /workers/ip/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json" 3 | } 4 | -------------------------------------------------------------------------------- /workers/ip/vitest.config.js: -------------------------------------------------------------------------------- 1 | import { defineWorkersConfig } from "@cloudflare/vitest-pool-workers/config"; 2 | 3 | export default defineWorkersConfig({ 4 | test: { 5 | poolOptions: { 6 | workers: { 7 | wrangler: { configPath: "./wrangler.toml" }, 8 | }, 9 | }, 10 | }, 11 | }); 12 | -------------------------------------------------------------------------------- /workers/ip/wrangler.toml: -------------------------------------------------------------------------------- 1 | name = "ip" 2 | account_id = "f5686db3c4f5b3e38b8f15b0561a28a8" 3 | compatibility_date = "2024-03-26" 4 | compatibility_flags = ["nodejs_compat"] 5 | 6 | main = "src/index.ts" 7 | 8 | routes = [{ pattern = "ip.mirio.dev", custom_domain = true }] 9 | -------------------------------------------------------------------------------- /workers/paste/README.md: -------------------------------------------------------------------------------- 1 | # Paste 2 | 3 | Share small snippets of text using the web! All Pastes expire after 7 days. 4 | 5 | Pastes is built using [Cloudflare Workers](https://workers.cloudflare.com/) and Cloudflare D1. 6 | 7 | ## Usage 8 | 9 | ### Create a new Paste 10 | 11 | ```bash 12 | curl -v https://paste.mirio.dev/pastes -H 'Content-Type: text/plain' -d 'Hello World!' 13 | 14 | {"url":"https://paste.mirio.dev/pastes/shwMg0"} 15 | ``` 16 | 17 | ### Get a Paste 18 | 19 | ```bash 20 | curl https://paste.mirio.dev/pastes/shwMg0 21 | 22 | Hello World! 23 | ``` 24 | -------------------------------------------------------------------------------- /workers/paste/migrations/001_schema.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS pastes; 2 | 3 | CREATE TABLE pastes ( 4 | id integer PRIMARY KEY, 5 | public_id text NOT NULL, 6 | content_type text NOT NULL, 7 | content text NOT NULL, 8 | created_at text NOT NULL, -- ISO 8601 9 | expires_at text -- ISO 8601, NULL if never expires 10 | ); 11 | 12 | CREATE INDEX IF NOT EXISTS pastes_public_id_idx ON pastes (public_id); -------------------------------------------------------------------------------- /workers/paste/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "paste", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "wrangler dev", 7 | "deploy": "wrangler deploy", 8 | "test": "vitest" 9 | }, 10 | "devDependencies": { 11 | "wrangler": "catalog:", 12 | "@cloudflare/vitest-pool-workers": "catalog:", 13 | "@cloudflare/workers-types": "catalog:", 14 | "vitest": "catalog:" 15 | }, 16 | "dependencies": { 17 | "hono": "^4.7.5", 18 | "short-unique-id": "^5.2.2" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /workers/paste/src/index.ts: -------------------------------------------------------------------------------- 1 | import { type Context, Hono } from "hono"; 2 | import { cors } from "hono/cors"; 3 | import { HTTPException } from "hono/http-exception"; 4 | import ShortUniqueId from "short-unique-id"; 5 | 6 | type Bindings = { 7 | HOST: string; 8 | DB: D1Database; 9 | }; 10 | 11 | interface Paste { 12 | id: number; 13 | public_id: string; 14 | content_type: string; 15 | content: string; 16 | created_at: string; 17 | expires_at: string; 18 | } 19 | 20 | const app = new Hono<{ Bindings: Bindings }>(); 21 | const uid = new ShortUniqueId(); 22 | 23 | app.all("*", cors()); 24 | 25 | app.get( 26 | "/pastes/:public_id{[0-9A-Za-z]+}", 27 | async (c: Context<{ Bindings: Bindings }>) => { 28 | const { public_id } = c.req.param(); 29 | const now = new Date().toISOString(); 30 | 31 | let result = null; 32 | try { 33 | result = await c.env.DB.prepare( 34 | "SELECT * FROM pastes WHERE public_id = ? AND expires_at > ?", 35 | ) 36 | .bind(public_id, now) 37 | .first(); 38 | } catch (e) { 39 | console.error(e); 40 | throw new HTTPException(500, { message: JSON.stringify(e) }); 41 | } 42 | 43 | if (!result) { 44 | throw new HTTPException(404, { message: "Paste not found" }); 45 | } 46 | 47 | return c.text(result.content, { 48 | headers: { "Content-Type": result.content_type }, 49 | }); 50 | }, 51 | ); 52 | 53 | app.get("/pastes", async (c: Context<{ Bindings: Bindings }>) => { 54 | const result = await c.env.DB.prepare("SELECT * FROM pastes").all(); 55 | if (result.error) { 56 | console.error(result.error); 57 | throw new HTTPException(500, { message: JSON.stringify(result.error) }); 58 | } 59 | 60 | const pastes: Paste[] = result.results.map((row) => ({ 61 | id: row.id as number, 62 | public_id: row.public_id as string, 63 | content_type: row.content_type as string, 64 | content: row.content as string, 65 | created_at: row.created_at as string, 66 | expires_at: row.expires_at as string, 67 | })); 68 | 69 | return c.json(pastes); 70 | }); 71 | 72 | app.post("/pastes", async (c: Context<{ Bindings: Bindings }>) => { 73 | const content_type = c.req.header("content-type"); 74 | const created_at = new Date().toISOString(); 75 | const expires_at = new Date( 76 | Date.now() + 1000 * 60 * 60 * 24 * 7, 77 | ).toISOString(); 78 | 79 | if (!content_type) { 80 | throw new HTTPException(400, { message: "Missing Content-Type header" }); 81 | } 82 | 83 | // only accept form data cont 84 | if ( 85 | !content_type.startsWith("multipart/form-data") && 86 | !content_type.startsWith("application/x-www-form-urlencoded") 87 | ) { 88 | console.log("invalid content type", content_type); 89 | throw new HTTPException(400, { message: "Invalid Content-Type header" }); 90 | } 91 | 92 | const form = await c.req.formData(); 93 | const content = form.get("content"); 94 | const public_id = uid.rnd(); 95 | 96 | let result = null; 97 | try { 98 | result = await c.env.DB.prepare( 99 | "INSERT INTO pastes (public_id, content_type, content, created_at, expires_at) VALUES (?, ?, ?, ?, ?) RETURNING public_id", 100 | ) 101 | .bind(public_id, content_type, content, created_at, expires_at) 102 | .first(); 103 | } catch (e) { 104 | console.error(e); 105 | throw new HTTPException(500, { message: JSON.stringify(e) }); 106 | } 107 | 108 | if (!result) { 109 | throw new HTTPException(500, { message: "Failed to create paste" }); 110 | } 111 | 112 | const host = c.env.HOST || "paste.mirio.dev"; 113 | 114 | return c.redirect(`/pastes/${result.public_id}`); 115 | }); 116 | 117 | export default { 118 | async scheduled(event, env: Bindings, ctx) { 119 | const result = await env.DB.prepare( 120 | "DELETE FROM pastes WHERE expires_at < ?", 121 | ) 122 | .bind(new Date().toISOString()) 123 | .run(); 124 | if (result.error) { 125 | console.error(result.error); 126 | throw new Error(result.error); 127 | } 128 | 129 | console.log( 130 | `Deleted ${result.meta.changes} pastes in ${result.meta.duration}ms`, 131 | ); 132 | }, 133 | ...app, 134 | }; 135 | -------------------------------------------------------------------------------- /workers/paste/test/apply-migrations.ts: -------------------------------------------------------------------------------- 1 | import { applyD1Migrations, env } from "cloudflare:test"; 2 | 3 | // Setup files run outside isolated storage, and may be run multiple times. 4 | // `applyD1Migrations()` only applies migrations that haven't already been 5 | // applied, therefore it is safe to call this function here. 6 | await applyD1Migrations(env.DB, env.TEST_MIGRATIONS); 7 | -------------------------------------------------------------------------------- /workers/paste/test/env.d.ts: -------------------------------------------------------------------------------- 1 | declare module "cloudflare:test" { 2 | // Controls the type of `import("cloudflare:test").env` 3 | interface ProvidedEnv extends Env { 4 | DB: D1Database; 5 | TEST_MIGRATIONS: D1Migration[]; // Defined in `vitest.config.ts` 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /workers/paste/test/index.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | createExecutionContext, 3 | env, 4 | waitOnExecutionContext, 5 | } from "cloudflare:test"; 6 | import { describe, expect, it } from "vitest"; 7 | // Could import any other source file/function here 8 | import worker from "../src"; 9 | 10 | describe("Echo worker", () => { 11 | it("responds with 200 OK", async () => { 12 | const request = new Request("http://example.com/pastes"); 13 | // Create an empty context to pass to `worker.fetch()` 14 | const ctx = createExecutionContext(); 15 | const response = await worker.fetch(request, env, ctx); 16 | // Wait for all `Promise`s passed to `ctx.waitUntil()` to settle before running test assertions 17 | await waitOnExecutionContext(ctx); 18 | expect(response.status).toBe(200); 19 | expect(await response.json()).to.length(0); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /workers/paste/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json" 3 | } 4 | -------------------------------------------------------------------------------- /workers/paste/vitest.config.js: -------------------------------------------------------------------------------- 1 | import path from "node:path"; 2 | import { 3 | defineWorkersProject, 4 | readD1Migrations, 5 | } from "@cloudflare/vitest-pool-workers/config"; 6 | 7 | export default defineWorkersProject(async () => { 8 | // Read all migrations in the `migrations` directory 9 | const migrationsPath = path.join(__dirname, "migrations"); 10 | const migrations = await readD1Migrations(migrationsPath); 11 | 12 | return { 13 | test: { 14 | setupFiles: ["./test/apply-migrations.ts"], 15 | poolOptions: { 16 | workers: { 17 | singleWorker: true, 18 | wrangler: { 19 | configPath: "./wrangler.toml", 20 | }, 21 | miniflare: { 22 | // Add a test-only binding for migrations, so we can apply them in a 23 | // setup file 24 | bindings: { TEST_MIGRATIONS: migrations }, 25 | }, 26 | }, 27 | }, 28 | }, 29 | }; 30 | }); 31 | -------------------------------------------------------------------------------- /workers/paste/wrangler.toml: -------------------------------------------------------------------------------- 1 | name = "paste-local" 2 | account_id = "f5686db3c4f5b3e38b8f15b0561a28a8" 3 | compatibility_date = "2024-03-26" 4 | compatibility_flags = ["nodejs_compat"] 5 | 6 | main = "src/index.ts" 7 | 8 | [vars] 9 | HOST = "localhost:8787" 10 | 11 | [[d1_databases]] 12 | binding = "DB" # i.e. available in your Worker on env.DB 13 | database_name = "paste-local" 14 | database_id = "04266953-209d-4313-b0a8-7b7f904ac3d1" 15 | 16 | # Staging 17 | [env.staging] 18 | name = "paste-staging" 19 | routes = [{ pattern = "paste-staging.mirio.dev", custom_domain = true }] 20 | vars = { HOST = "paste-staging.mirio.dev" } 21 | 22 | [[env.staging.d1_databases]] 23 | binding = "DB" # i.e. available in your Worker on env.DB 24 | database_name = "paste-staging-db" 25 | database_id = "e027ed0d-f9f1-49f6-ba7c-21eecbc1a4e3" 26 | 27 | # Production 28 | [env.production] 29 | name = "paste" 30 | routes = [{ pattern = "paste.mirio.dev", custom_domain = true }] 31 | vars = { HOST = "paste.mirio.dev" } 32 | 33 | [[env.production.d1_databases]] 34 | binding = "DB" # i.e. available in your Worker on env.DB 35 | database_name = "paste-db" 36 | database_id = "7dab7c92-2054-44e2-a7ba-07591f339be8" 37 | 38 | # Cleanup old pastes every 30 minutes 39 | [env.production.triggers] 40 | crons = ["*/30 * * * *"] 41 | -------------------------------------------------------------------------------- /workers/rand/README.md: -------------------------------------------------------------------------------- 1 | # `rand` 🎲 2 | 3 | Get random values! 4 | 5 | ``` 6 | curl https://rand.mirio.dev/uuid 7 | cf17cfa2-32c5-4182-b81b-983b28cb9fa8 8 | 9 | curl https://rand.mirio.dev/uuid?n=2 10 | cf17cfa2-32c5-4182-b81b-983b28cb9fa8 11 | 4610fb40-1e70-426e-807f-2a036df4be73 12 | ``` 13 | -------------------------------------------------------------------------------- /workers/rand/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rand", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "wrangler dev", 7 | "deploy": "wrangler deploy", 8 | "test": "vitest" 9 | }, 10 | "devDependencies": { 11 | "wrangler": "catalog:", 12 | "@cloudflare/vitest-pool-workers": "catalog:", 13 | "@cloudflare/workers-types": "catalog:", 14 | "vitest": "catalog:" 15 | }, 16 | "dependencies": { 17 | "itty-router": "^5.0.18" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /workers/rand/src/index.ts: -------------------------------------------------------------------------------- 1 | import { type IRequest, Router } from "itty-router"; 2 | 3 | const router = Router(); 4 | 5 | router.get("/uuid", (request: IRequest) => { 6 | const { searchParams } = new URL(request.url); 7 | const count: number = Number.parseInt( 8 | searchParams.get("n") || 9 | searchParams.get("count") || 10 | searchParams.get("limit") || 11 | "1", 12 | 10, 13 | ); 14 | const data = Array(count) 15 | .fill(0) 16 | .map(() => crypto.randomUUID()); 17 | return new Response(`${data.join("\n")}\n`); 18 | }); 19 | 20 | router.get("*", () => new Response("Not found.\n", { status: 404 })); 21 | 22 | export default router; 23 | -------------------------------------------------------------------------------- /workers/rand/test/index.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | createExecutionContext, 3 | env, 4 | waitOnExecutionContext, 5 | } from "cloudflare:test"; 6 | import { describe, expect, it } from "vitest"; 7 | // Could import any other source file/function here 8 | import worker from "../src"; 9 | 10 | describe("Echo worker", () => { 11 | it("responds with 200 OK", async () => { 12 | const request = new Request("http://example.com/uuid"); 13 | // Create an empty context to pass to `worker.fetch()` 14 | const ctx = createExecutionContext(); 15 | const response = await worker.fetch(request, env, ctx); 16 | // Wait for all `Promise`s passed to `ctx.waitUntil()` to settle before running test assertions 17 | await waitOnExecutionContext(ctx); 18 | expect(response.status).toBe(200); 19 | const body = await response.text(); 20 | expect(body).to.match( 21 | /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\n$/, 22 | ); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /workers/rand/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json" 3 | } 4 | -------------------------------------------------------------------------------- /workers/rand/vitest.config.js: -------------------------------------------------------------------------------- 1 | import { defineWorkersConfig } from "@cloudflare/vitest-pool-workers/config"; 2 | 3 | export default defineWorkersConfig({ 4 | test: { 5 | poolOptions: { 6 | workers: { 7 | wrangler: { configPath: "./wrangler.toml" }, 8 | }, 9 | }, 10 | }, 11 | }); 12 | -------------------------------------------------------------------------------- /workers/rand/wrangler.toml: -------------------------------------------------------------------------------- 1 | name = "rand" 2 | account_id = "f5686db3c4f5b3e38b8f15b0561a28a8" 3 | compatibility_date = "2024-03-26" 4 | compatibility_flags = ["nodejs_compat"] 5 | 6 | main = "src/index.ts" 7 | 8 | routes = [{ pattern = "rand.mirio.dev", custom_domain = true }] 9 | -------------------------------------------------------------------------------- /workers/rdap/README.md: -------------------------------------------------------------------------------- 1 | # RDAP 2 | 3 | Query RDAP (Registration Data Access Protocol) servers for domain registration information using `curl`. 4 | 5 | ## API 6 | 7 | - **GET /:domain** - Query RDAP server for domain registration information. 8 | 9 | The response is a `key=value` list of domain registration information. Any lists are comma-separated. See example below for more information. 10 | 11 | ## Usage 12 | 13 | ```bash 14 | curl -s https://rdap.mirio.dev/google.com 15 | 16 | domain=google.com 17 | registered_at=1997-09-15T04:00:00Z 18 | expires_at=1997-09-15T04:00:00Z 19 | last_changed=2019-09-09T15:39:04Z 20 | last_updated=2024-04-12T02:50:28Z 21 | name_servers=NS1.GOOGLE.COM,NS2.GOOGLE.COM,NS3.GOOGLE.COM,NS4.GOOGLE.COM 22 | status=client delete prohibited,client transfer prohibited,client update prohibited,server delete prohibited,server transfer prohibited,server update prohibited 23 | ``` 24 | 25 | -------------------------------------------------------------------------------- /workers/rdap/migrations/001_schema.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS pastes; 2 | 3 | CREATE TABLE pastes ( 4 | id integer PRIMARY KEY, 5 | public_id text NOT NULL, 6 | content_type text NOT NULL, 7 | content text NOT NULL, 8 | created_at text NOT NULL, -- ISO 8601 9 | expires_at text -- ISO 8601, NULL if never expires 10 | ); 11 | 12 | CREATE INDEX IF NOT EXISTS pastes_public_id_idx ON pastes (public_id); -------------------------------------------------------------------------------- /workers/rdap/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rdap", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "wrangler dev", 7 | "deploy": "wrangler deploy", 8 | "test": "vitest" 9 | }, 10 | "devDependencies": { 11 | "wrangler": "catalog:", 12 | "@cloudflare/vitest-pool-workers": "catalog:", 13 | "@cloudflare/workers-types": "catalog:", 14 | "vitest": "catalog:" 15 | }, 16 | "dependencies": { 17 | "hono": "^4.7.5" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /workers/rdap/src/index.ts: -------------------------------------------------------------------------------- 1 | import { type Context, Hono } from "hono"; 2 | import { cors } from "hono/cors"; 3 | import { HTTPException } from "hono/http-exception"; 4 | 5 | type Bindings = { 6 | HOST: string; 7 | UPSTREAM: KVNamespace; 8 | DB: D1Database; 9 | }; 10 | const app = new Hono<{ Bindings: Bindings }>(); 11 | 12 | app.all("*", cors()); 13 | 14 | app.get("/:domain", async (c: Context<{ Bindings: Bindings }>) => { 15 | const { domain } = c.req.param(); 16 | 17 | let dnsJson: RDAPBooststrap = await c.env.UPSTREAM.get("dns.json", "json"); 18 | if (!dnsJson) { 19 | console.log("Fetching RDAP bootstrap data"); 20 | const resp = await fetch("https://data.iana.org/rdap/dns.json"); 21 | if (!resp.ok) { 22 | throw new HTTPException(500, { 23 | message: "Failed to fetch RDAP bootstrap data", 24 | }); 25 | } 26 | dnsJson = await resp.json(); 27 | await c.env.UPSTREAM.put("dns.json", JSON.stringify(dnsJson), { 28 | expirationTtl: 86400, 29 | }); 30 | console.log("RDAP bootstrap data cached for 24 hours"); 31 | } 32 | 33 | const tld = domain.split(".").slice(-1)[0]; 34 | const service = dnsJson.services.find((service) => service[0].includes(tld)); 35 | if (!service || service.length === 0) { 36 | throw new HTTPException(404, { 37 | message: "No RDAP service found for this domain", 38 | }); 39 | } 40 | 41 | const serviceUrls = service[1]; 42 | 43 | let rdapData = null; 44 | for (const url of serviceUrls) { 45 | const u = new URL(`domain/${domain}`, url); 46 | console.log("Trying", u.toString()); 47 | const resp = await fetch(u.toString()); 48 | if (!resp.ok) { 49 | console.log("Failed", resp.status, resp.statusText); 50 | } else { 51 | rdapData = await resp.json(); 52 | } 53 | } 54 | 55 | if (!rdapData) { 56 | throw new HTTPException(404, { 57 | message: "No RDAP data found for this domain", 58 | }); 59 | } 60 | 61 | const registration = rdapData?.events?.find( 62 | (event) => event.eventAction === "registration", 63 | )?.eventDate; 64 | const expiresAt = rdapData?.events?.find( 65 | (event) => event.eventAction === "expiration", 66 | )?.eventDate; 67 | const lastChanged = rdapData?.events?.find( 68 | (event) => event.eventAction === "last changed", 69 | )?.eventDate; 70 | const lastUpdated = rdapData?.events?.find( 71 | (event) => event.eventAction === "last update of RDAP database", 72 | )?.eventDate; 73 | const nameServers = rdapData?.nameservers?.map((ns) => ns.ldhName); 74 | const status = rdapData?.status; 75 | 76 | return c.text( 77 | `domain=${domain}\n` + 78 | `registered_at=${registration}\n` + 79 | `expires_at=${expiresAt}\n` + 80 | `last_changed=${lastChanged}\n` + 81 | `last_rdap_update=${lastUpdated}\n` + 82 | `name_servers=${nameServers.join(",")}\n` + 83 | `status=${status.join(",")}`, 84 | ); 85 | }); 86 | 87 | export default { 88 | ...app, 89 | }; 90 | 91 | export interface RDAPBooststrap { 92 | description: string; 93 | publication: Date; 94 | services: Array>; 95 | version: string; 96 | } 97 | -------------------------------------------------------------------------------- /workers/rdap/test/index.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | createExecutionContext, 3 | env, 4 | waitOnExecutionContext, 5 | } from "cloudflare:test"; 6 | import { describe, expect, it } from "vitest"; 7 | // Could import any other source file/function here 8 | import worker from "../src"; 9 | 10 | describe("Echo worker", () => { 11 | it("responds with 200 OK", async () => { 12 | const request = new Request("http://example.com/google.com"); 13 | // Create an empty context to pass to `worker.fetch()` 14 | const ctx = createExecutionContext(); 15 | const response = await worker.fetch(request, env, ctx); 16 | // Wait for all `Promise`s passed to `ctx.waitUntil()` to settle before running test assertions 17 | await waitOnExecutionContext(ctx); 18 | expect(response.status).toBe(200); 19 | expect(await response.text()).to.be.not.empty; 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /workers/rdap/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json" 3 | } 4 | -------------------------------------------------------------------------------- /workers/rdap/vitest.config.js: -------------------------------------------------------------------------------- 1 | import { defineWorkersConfig } from "@cloudflare/vitest-pool-workers/config"; 2 | 3 | export default defineWorkersConfig({ 4 | test: { 5 | poolOptions: { 6 | workers: { 7 | wrangler: { configPath: "./wrangler.toml" }, 8 | }, 9 | }, 10 | }, 11 | }); 12 | -------------------------------------------------------------------------------- /workers/rdap/wrangler.toml: -------------------------------------------------------------------------------- 1 | name = "rdap" 2 | account_id = "f5686db3c4f5b3e38b8f15b0561a28a8" 3 | compatibility_date = "2024-03-26" 4 | compatibility_flags = ["nodejs_compat"] 5 | 6 | main = "src/index.ts" 7 | routes = [{ pattern = "rdap.mirio.dev", custom_domain = true }] 8 | 9 | [[kv_namespaces]] 10 | id = "2fbd241b8f2349e5993d39a2208be142" 11 | binding = "UPSTREAM" 12 | 13 | [env.production] 14 | name = "rdap" 15 | workers_dev = false 16 | -------------------------------------------------------------------------------- /workers/stocks/README.md: -------------------------------------------------------------------------------- 1 | # `stocks` 📈 2 | 3 | Get quick information about your favorite stock symbols! 4 | 5 | ## Quote 6 | 7 | ```bash 8 | curl -s https://stocks.mirio.dev/NET | jq 9 | { 10 | "date": "2022-08-26", 11 | "symbol": "NET", 12 | "currencyCode": "USD", 13 | "currencySymbol": "$", 14 | "price": 64.39, 15 | "open": 68.02999877929688, 16 | "high": 68.25, 17 | "low": 64.25, 18 | "close": 64.38999938964844, 19 | "previousClose": 68.24 20 | } 21 | ``` 22 | -------------------------------------------------------------------------------- /workers/stocks/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stocks", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "wrangler dev", 7 | "deploy": "wrangler deploy", 8 | "test": "vitest" 9 | }, 10 | "devDependencies": { 11 | "wrangler": "catalog:", 12 | "@cloudflare/vitest-pool-workers": "catalog:", 13 | "@cloudflare/workers-types": "catalog:", 14 | "vitest": "catalog:" 15 | }, 16 | "dependencies": { 17 | "itty-router": "^5.0.18" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /workers/stocks/src/common/quote.ts: -------------------------------------------------------------------------------- 1 | export interface Quote { 2 | // Date of the quote in yyyy-mm-dd format 3 | date: string; 4 | // The trading symbol 5 | symbol: string; 6 | // Price at opening 7 | open: number; 8 | // Highest price of the day 9 | high: number; 10 | // Lowest price of the day 11 | low: number; 12 | // Current trading price 13 | price: number; 14 | // Previous day closing price 15 | previousClose: number; 16 | // Currency symbol to be used when formatting prices. 17 | currencySymbol: "$"; 18 | // currencyCode: ISO 4217 https://en.wikipedia.org/wiki/ISO_4217 19 | currencyCode: "USD"; 20 | } 21 | -------------------------------------------------------------------------------- /workers/stocks/src/index.ts: -------------------------------------------------------------------------------- 1 | import { type IRequest, Router } from "itty-router"; 2 | import { YahooFinance } from "./yahoofinance"; 3 | 4 | export interface StockQuote { 5 | date: string; 6 | symbol: string; 7 | currencyCode: string; 8 | currencySymbol: string; 9 | price: number; 10 | open: number; 11 | high: number; 12 | low: number; 13 | close: number; 14 | previousClose: number; 15 | } 16 | 17 | const router = Router(); 18 | 19 | router.get("/:symbol", async (request: IRequest) => { 20 | const { params } = request; 21 | const yahoo = new YahooFinance(); 22 | return Response.json(await yahoo.quote(params.symbol)); 23 | }); 24 | 25 | export default router; 26 | -------------------------------------------------------------------------------- /workers/stocks/src/yahoofinance/index.ts: -------------------------------------------------------------------------------- 1 | import type { Quote } from "../common/quote"; 2 | 3 | interface QuoteResult { 4 | meta: { 5 | currency: string; 6 | symbol: string; 7 | exchangeName: string; 8 | regularMarketPrice: number; 9 | previousClose: number; 10 | regularMarketTime: number; 11 | exchangeTimezoneName: string; 12 | }; 13 | indicators: { 14 | quote: { 15 | high: (number | null)[]; 16 | close: (number | null)[]; 17 | open: (number | null)[]; 18 | low: (number | null)[]; 19 | }[]; 20 | }; 21 | } 22 | 23 | interface QuoteError { 24 | code: string; 25 | description: string; 26 | } 27 | 28 | interface QuoteResponse { 29 | chart: 30 | | { 31 | result: QuoteResult[]; 32 | error: null; 33 | } 34 | | { 35 | result: null; 36 | error: QuoteError; 37 | }; 38 | } 39 | 40 | export class YahooFinance { 41 | private readonly url = "https://query1.finance.yahoo.com"; 42 | 43 | async quote(symbol: string, date?: string): Promise { 44 | const url = new URL(`/v8/finance/chart/${symbol}`, this.url); 45 | console.log(url.toString()); 46 | const resp = await fetch(url.toString(), { 47 | headers: { 48 | "User-Agent": 49 | "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36", 50 | }, 51 | }); 52 | if (!resp.ok) { 53 | console.log("Failed to fetch quote", resp.status, resp.statusText); 54 | return null; 55 | } 56 | 57 | const data: QuoteResponse = await resp.json(); 58 | if (data.chart.error) { 59 | return null; 60 | } 61 | const result = data.chart.result.pop(); 62 | if (!result) { 63 | return null; 64 | } 65 | 66 | const quote = result.indicators.quote.pop(); 67 | if (!quote) { 68 | return null; 69 | } 70 | 71 | return { 72 | // use `en-CA` for `yyyy-mm-dd` format 73 | date: new Date(result.meta.regularMarketTime * 1000).toLocaleDateString( 74 | "en-CA", 75 | { 76 | timeZone: result.meta.exchangeTimezoneName, 77 | }, 78 | ), 79 | symbol: result.meta.symbol, 80 | currencyCode: result.meta.currency, 81 | currencySymbol: "$", 82 | price: result.meta.regularMarketPrice, 83 | open: quote.open.filter(Boolean)[0] || null, 84 | high: Math.max(...(quote.high.filter(Boolean) as number[])), 85 | low: Math.min(...(quote.low.filter(Boolean) as number[])), 86 | close: quote.close.filter(Boolean).pop(), 87 | previousClose: result.meta.previousClose, 88 | } as Quote; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /workers/stocks/test/index.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | createExecutionContext, 3 | env, 4 | waitOnExecutionContext, 5 | } from "cloudflare:test"; 6 | import { describe, expect, it } from "vitest"; 7 | // Could import any other source file/function here 8 | import worker, { type StockQuote } from "../src"; 9 | 10 | describe("Echo worker", () => { 11 | it("responds with 200 OK", async () => { 12 | const request = new Request("http://example.com/NET"); 13 | // Create an empty context to pass to `worker.fetch()` 14 | const ctx = createExecutionContext(); 15 | const response = await worker.fetch(request, env, ctx); 16 | // Wait for all `Promise`s passed to `ctx.waitUntil()` to settle before running test assertions 17 | await waitOnExecutionContext(ctx); 18 | expect(response.status).toBe(200); 19 | const body = (await response.json()) as StockQuote; 20 | expect(body).toBeDefined(); 21 | expect(body.symbol).toBe("NET"); 22 | expect(body.price).toBeGreaterThan(0); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /workers/stocks/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json" 3 | } 4 | -------------------------------------------------------------------------------- /workers/stocks/vitest.config.js: -------------------------------------------------------------------------------- 1 | import { defineWorkersConfig } from "@cloudflare/vitest-pool-workers/config"; 2 | 3 | export default defineWorkersConfig({ 4 | test: { 5 | poolOptions: { 6 | workers: { 7 | wrangler: { configPath: "./wrangler.toml" }, 8 | }, 9 | }, 10 | }, 11 | }); 12 | -------------------------------------------------------------------------------- /workers/stocks/wrangler.toml: -------------------------------------------------------------------------------- 1 | name = "stocks" 2 | account_id = "f5686db3c4f5b3e38b8f15b0561a28a8" 3 | compatibility_date = "2024-03-26" 4 | compatibility_flags = ["nodejs_compat"] 5 | 6 | main = "src/index.ts" 7 | 8 | routes = [{ pattern = "stocks.mirio.dev", custom_domain = true }] 9 | -------------------------------------------------------------------------------- /workers/tfstate/README.md: -------------------------------------------------------------------------------- 1 | # `tfstate` ⛏ 2 | 3 | Managed Terraform state using the [`http` backend](https://www.terraform.io/language/settings/backends/http), backed by 4 | Cloudflare Workers. Oh, and it supports locking 🔒. 5 | 6 | ## Getting Started 7 | 8 | ```terraform 9 | terraform { 10 | backend "http" { 11 | address = "https://apigw.eragon.xyz/tfstate/states/your-project-name" 12 | lock_address = "https://apigw.eragon.xyz/tfstate/states/your-project-name/lock" 13 | lock_method = "PUT" # also supports default "LOCK" 14 | unlock_address = "https://apigw.eragon.xyz/tfstate/states/your-project-name/lock" 15 | unlock_method = "DELETE" # also supports default "UNLOCK" 16 | username = "" 17 | password = "" 18 | } 19 | } 20 | ``` 21 | 22 | ## Troubleshooting 23 | 24 | Got yourself in a tfstate*tastrophy*? The following commands may help. 25 | 26 | **NOTE: These can be destructive, so be careful!** 27 | 28 | ### My state is locked, how can I unlock it? 29 | 30 | ```curl 31 | # Get current lock info 32 | curl https://apigw.eragon.xyz/tfstate/states/your-project-name/lock 33 | 34 | # Manually remove the lock (similar to `terraform force-unlock`) 35 | curl -X DELETE https://apigw.eragon.xyz/tfstate/states/your-project-name/lock 36 | ``` 37 | 38 | ### I get a 400 Error when attempting to lock 39 | 40 | Double check you are using UPPER case values for `lock_method` and `unlock_method`. 41 | -------------------------------------------------------------------------------- /workers/tfstate/example/.terraform.lock.hcl: -------------------------------------------------------------------------------- 1 | # This file is maintained automatically by "terraform init". 2 | # Manual edits may be lost in future updates. 3 | 4 | provider "registry.terraform.io/hashicorp/http" { 5 | version = "3.0.1" 6 | constraints = "3.0.1" 7 | hashes = [ 8 | "h1:wEnSFj3SX3kPMQAv8o66FzFAc/G4CD5j4U0w0sxtQDY=", 9 | "zh:3b161998147d8cc3986a1580ddb065009ab628747424934cbcb9d221783541f8", 10 | "zh:62c78b565cde08d8e3b98e8138cd8e46b50fdc2ddc560ac1f62b5646ce8e9b1f", 11 | "zh:69ba560cd6360a285e83e1c220ab140d3119371850756ff2ed0abe39d362ea49", 12 | "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", 13 | "zh:95f38aebfa176a3424a329bc0f2e958bcf5a1f98d91dee21a436ca670fb2d570", 14 | "zh:97eae729eb859948201d4393761f5c1a7ffe84046473527f65163f062d9af5d9", 15 | "zh:b42de839114707e2fcfdf5ebf3a89129e5e17ebb5f84651c5775daecd776dc3b", 16 | "zh:c47fa93605b8378504008534e0057e295d209a2128553c7b1bcc4fc7f6efafa2", 17 | "zh:d9d4fe5143f80c1ccf22b055f069445ab7470942bb46027dadda8f3bc62d2780", 18 | "zh:f051820764c50f4736d21e40d9b13a1ffde678748a9e6e1ef22a26adf27db9bf", 19 | "zh:f67c9b73998fce13e94623be9b7afe89b30e3e6d34b504f765a344b11b8808b8", 20 | "zh:f7d255dac5a73d30c7e629699fdf064decf705cd701d29e2120cef7bf0fb1d7f", 21 | ] 22 | } 23 | 24 | provider "registry.terraform.io/hashicorp/random" { 25 | version = "3.3.2" 26 | constraints = "3.3.2" 27 | hashes = [ 28 | "h1:YChjos7Hrvr2KgTc9GzQ+de/QE2VLAeRJgxFemnCltU=", 29 | "zh:038293aebfede983e45ee55c328e3fde82ae2e5719c9bd233c324cfacc437f9c", 30 | "zh:07eaeab03a723d83ac1cc218f3a59fceb7bbf301b38e89a26807d1c93c81cef8", 31 | "zh:427611a4ce9d856b1c73bea986d841a969e4c2799c8ac7c18798d0cc42b78d32", 32 | "zh:49718d2da653c06a70ba81fd055e2b99dfd52dcb86820a6aeea620df22cd3b30", 33 | "zh:5574828d90b19ab762604c6306337e6cd430e65868e13ef6ddb4e25ddb9ad4c0", 34 | "zh:7222e16f7833199dabf1bc5401c56d708ec052b2a5870988bc89ff85b68a5388", 35 | "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", 36 | "zh:b1b2d7d934784d2aee98b0f8f07a8ccfc0410de63493ae2bf2222c165becf938", 37 | "zh:b8f85b6a20bd264fcd0814866f415f0a368d1123cd7879c8ebbf905d370babc8", 38 | "zh:c3813133acc02bbebddf046d9942e8ba5c35fc99191e3eb057957dafc2929912", 39 | "zh:e7a41dbc919d1de800689a81c240c27eec6b9395564630764ebb323ea82ac8a9", 40 | "zh:ee6d23208449a8eaa6c4f203e33f5176fa795b4b9ecf32903dffe6e2574732c2", 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /workers/tfstate/example/errored.tfstate: -------------------------------------------------------------------------------- 1 | { 2 | "version": 4, 3 | "terraform_version": "1.2.9", 4 | "serial": 3, 5 | "lineage": "f42aba9f-13a1-06e9-2267-10eb9fb08e7a", 6 | "outputs": { 7 | "pet": { 8 | "value": "keen-terrier", 9 | "type": "string" 10 | }, 11 | "pwd": { 12 | "value": "W=+xKI2E}$*T\u003eXk", 13 | "type": "string", 14 | "sensitive": true 15 | } 16 | }, 17 | "resources": [ 18 | { 19 | "mode": "managed", 20 | "type": "random_password", 21 | "name": "server", 22 | "provider": "provider[\"registry.terraform.io/hashicorp/random\"]", 23 | "instances": [ 24 | { 25 | "schema_version": 2, 26 | "attributes": { 27 | "bcrypt_hash": "$2a$10$YbIbIdYbBnXNDZFduj5FCumxy526VgcckC0GAwwvRhhhi3q8bRZKy", 28 | "id": "none", 29 | "keepers": null, 30 | "length": 15, 31 | "lower": true, 32 | "min_lower": 0, 33 | "min_numeric": 0, 34 | "min_special": 0, 35 | "min_upper": 0, 36 | "number": true, 37 | "numeric": true, 38 | "override_special": null, 39 | "result": "W=+xKI2E}$*T\u003eXk", 40 | "special": true, 41 | "upper": true 42 | }, 43 | "sensitive_attributes": [], 44 | "private": "eyJzY2hlbWFfdmVyc2lvbiI6IjIifQ==" 45 | } 46 | ] 47 | }, 48 | { 49 | "mode": "managed", 50 | "type": "random_pet", 51 | "name": "server", 52 | "provider": "provider[\"registry.terraform.io/hashicorp/random\"]", 53 | "instances": [ 54 | { 55 | "schema_version": 0, 56 | "attributes": { 57 | "id": "keen-terrier", 58 | "keepers": null, 59 | "length": 2, 60 | "prefix": null, 61 | "separator": "-" 62 | }, 63 | "sensitive_attributes": [], 64 | "private": "bnVsbA==" 65 | } 66 | ] 67 | } 68 | ] 69 | } 70 | -------------------------------------------------------------------------------- /workers/tfstate/example/main.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | backend "http" { 3 | address = "https://apigw.eragon.xyz/tfstate/states/tfstate-example" 4 | lock_address = "https://apigw.eragon.xyz/tfstate/states/tfstate-example/lock" 5 | lock_method = "PUT" 6 | unlock_address = "https://apigw.eragon.xyz/tfstate/states/tfstate-example/lock" 7 | unlock_method = "DELETE" 8 | username = "e0f30419b4cca01ba2931345a61cc592.access" 9 | } 10 | 11 | required_providers { 12 | http = { 13 | source = "hashicorp/http" 14 | version = "3.0.1" 15 | } 16 | random = { 17 | source = "hashicorp/random" 18 | version = "3.3.2" 19 | } 20 | } 21 | } 22 | 23 | resource "random_pet" "server" { 24 | 25 | } 26 | 27 | output "pet" { 28 | value = random_pet.server.id 29 | } 30 | 31 | resource "random_password" "server" { 32 | length = 15 33 | } 34 | 35 | output "pwd" { 36 | value = random_password.server.result 37 | sensitive = true 38 | } 39 | -------------------------------------------------------------------------------- /workers/tfstate/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tfstate", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "wrangler dev", 7 | "deploy": "wrangler deploy", 8 | "test": "vitest" 9 | }, 10 | "devDependencies": { 11 | "wrangler": "catalog:", 12 | "@cloudflare/vitest-pool-workers": "catalog:", 13 | "@cloudflare/workers-types": "catalog:", 14 | "vitest": "catalog:" 15 | }, 16 | "dependencies": { 17 | "itty-router": "^5.0.18", 18 | "jose": "^6.0.10" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /workers/tfstate/src/durableLock.ts: -------------------------------------------------------------------------------- 1 | import { Router } from "itty-router"; 2 | import type { Env } from "./types/env"; 3 | import type { LockInfo } from "./types/terraform"; 4 | 5 | export class DurableLock { 6 | private state: DurableObjectState; 7 | private lockInfo: LockInfo | null; 8 | 9 | constructor(state: DurableObjectState, env: Env) { 10 | this.state = state; 11 | this.lockInfo = null; 12 | this.state.blockConcurrencyWhile(async () => { 13 | this.lockInfo = (await this.state.storage.get("_lock")) || null; 14 | }); 15 | } 16 | 17 | async fetch(request: Request): Promise { 18 | const router = Router(); 19 | // non-standard routes 20 | router.get("/states/:projectName/lock", this.currentLockInfo.bind(this)); 21 | 22 | // Lock 23 | router.put("/states/:projectName/lock", this.lock.bind(this)); 24 | router.lock("/states/:projectName/lock", this.lock.bind(this)); 25 | 26 | // Unlock 27 | router.delete("/states/:projectName/lock", this.unlock.bind(this)); 28 | router.unlock("/states/:projectName/lock", this.unlock.bind(this)); 29 | router.all("*", () => new Response(request.url, { status: 404 })); 30 | 31 | return router.fetch(request); 32 | } 33 | 34 | private async lock(request: Request): Promise { 35 | if (this.lockInfo) return Response.json(this.lockInfo, { status: 423 }); 36 | const lockInfo = (await request.json()) as LockInfo; 37 | await this.state.storage.put("_lock", lockInfo); 38 | this.lockInfo = lockInfo; 39 | return new Response(); 40 | } 41 | 42 | private async unlock(request: Request): Promise { 43 | const lockInfo = (await request.json()) as LockInfo; 44 | if (!lockInfo.ID) 45 | return new Response("Missing ID for unlock state request", { 46 | status: 400, 47 | }); 48 | if (this.lockInfo?.ID !== lockInfo.ID) 49 | return Response.json(this.lockInfo, { status: 423 }); 50 | await this.state.storage.delete("_lock"); 51 | this.lockInfo = null; 52 | return new Response(); 53 | } 54 | 55 | private async currentLockInfo(request: Request): Promise { 56 | return Response.json(this.lockInfo || {}); 57 | } 58 | 59 | private async purge(request: Request): Promise { 60 | this.state.storage.deleteAll(); 61 | this.lockInfo = null; 62 | return new Response(); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /workers/tfstate/src/handlers.ts: -------------------------------------------------------------------------------- 1 | import type { Env } from "./types/env"; 2 | import type { RequestWithIdentity } from "./types/request"; 3 | import type { LockInfo } from "./types/terraform"; 4 | import { getObjectKey } from "./utils"; 5 | 6 | /** 7 | * Returns the current remote state from the durable storage. Doesn't support locking 8 | * GET /states/:projectName 9 | * https://github.com/hashicorp/terraform/blob/cb340207d8840f3d2bc5dab100a5813d1ea3122b/internal/backend/remote-state/http/client.go#L144 10 | * @param request 11 | * @param env 12 | */ 13 | export const getStateHandler = async ( 14 | request: RequestWithIdentity, 15 | env: Env, 16 | ) => { 17 | const { projectName } = request; 18 | const username = request.identity?.userInfo?.username || ""; 19 | if (!projectName || projectName === "") 20 | return new Response("No project name specified.", { status: 400 }); 21 | if (!username || username === "") 22 | return new Response("Unable to determine username", { status: 500 }); 23 | const state: R2ObjectBody = await env.TFSTATE_BUCKET.get( 24 | getObjectKey(username, projectName), 25 | ); 26 | if (state === null) return new Response(null, { status: 204 }); 27 | return new Response(await state?.arrayBuffer(), { 28 | headers: { "content-type": "application/json" }, 29 | }); 30 | }; 31 | 32 | /** 33 | * Updates the remote state in the durable storage. Supports locking. 34 | * POST /states/:projectName?ID= 35 | * https://github.com/hashicorp/terraform/blob/cb340207d8840f3d2bc5dab100a5813d1ea3122b/internal/backend/remote-state/http/client.go#L203 36 | * @param request 37 | * @param env 38 | */ 39 | export const putStateHandler = async ( 40 | request: RequestWithIdentity, 41 | env: Env, 42 | ) => { 43 | const { projectName, url } = request; 44 | const username = request.identity?.userInfo?.username || ""; 45 | if (!projectName || projectName === "") 46 | return new Response("No project name specified.", { status: 400 }); 47 | if (!username || username === "") 48 | return new Response("Unable to determine username", { status: 500 }); 49 | 50 | const key = getObjectKey(username, projectName); 51 | const id = env.TFSTATE_LOCK.idFromName(key); 52 | const lock = env.TFSTATE_LOCK.get(id); 53 | const lockResp = await lock.fetch( 54 | `https://lock.do/states/${projectName}/lock`, 55 | ); 56 | const lockInfo = (await lockResp.json()) as LockInfo; 57 | 58 | // Lock present, ensure the update request has the correct lock ID 59 | if (lockInfo.ID) { 60 | const lockId = new URL(url).searchParams.get("ID"); 61 | if (lockInfo.ID !== lockId) return Response.json(lockInfo, { status: 423 }); 62 | } 63 | 64 | await env.TFSTATE_BUCKET.put(key, await request.arrayBuffer()); 65 | return new Response(); 66 | }; 67 | 68 | /** 69 | * Deletes the remote state in the durable storage. 70 | * Does not support/honor locking. 71 | * DELETE /states/:projectName 72 | * https://github.com/hashicorp/terraform/blob/cb340207d8840f3d2bc5dab100a5813d1ea3122b/internal/backend/remote-state/http/client.go#L241 73 | * @param request 74 | * @param env 75 | */ 76 | export const deleteStateHandler = async ( 77 | request: RequestWithIdentity, 78 | env: Env, 79 | ) => { 80 | const { projectName, url } = request; 81 | const username = request.identity?.userInfo?.username || ""; 82 | if (!projectName || projectName === "") 83 | return new Response("No project name specified.", { status: 400 }); 84 | if (!username || username === "") 85 | return new Response("Unable to determine username", { status: 500 }); 86 | 87 | const key = getObjectKey(username, projectName); 88 | const id = env.TFSTATE_LOCK.idFromName(key); 89 | const lock = env.TFSTATE_LOCK.get(id); 90 | const lockResp = await lock.fetch( 91 | `https://lock.do/states/${projectName}/lock`, 92 | ); 93 | const lockInfo = (await lockResp.json()) as LockInfo; 94 | 95 | // Lock present, prevent delete entirely. 96 | if (lockInfo.ID) return Response.json(lockInfo, { status: 423 }); 97 | 98 | await env.TFSTATE_BUCKET.delete(getObjectKey(username, projectName)); 99 | return new Response(); 100 | }; 101 | 102 | /** 103 | * Lock or Unlock the remote state for edits. 104 | * PUT/DELETE /states/:projectName/lock 105 | * @param request 106 | */ 107 | export const lockStateHandler = async ( 108 | request: RequestWithIdentity, 109 | env: Env, 110 | ) => { 111 | const { projectName } = request; 112 | const username = request.identity?.userInfo?.username || ""; 113 | if (!projectName || projectName === "") 114 | return new Response("No project name specified.", { status: 400 }); 115 | if (!username || username === "") 116 | return new Response("Unable to determine username", { status: 500 }); 117 | const id = env.TFSTATE_LOCK.idFromName(getObjectKey(username, projectName)); 118 | const lock = env.TFSTATE_LOCK.get(id); 119 | return lock.fetch(request); 120 | }; 121 | -------------------------------------------------------------------------------- /workers/tfstate/src/index.ts: -------------------------------------------------------------------------------- 1 | import { Router } from "itty-router"; 2 | import { 3 | deleteStateHandler, 4 | getStateHandler, 5 | lockStateHandler, 6 | putStateHandler, 7 | } from "./handlers"; 8 | import { withIdentity, withParams } from "./middlewares"; 9 | import type { Env } from "./types/env"; 10 | 11 | export { DurableLock } from "./durableLock"; 12 | 13 | const router = Router(); 14 | 15 | export default { 16 | async fetch( 17 | request: Request, 18 | env: Env, 19 | ctx: ExecutionContext, 20 | ): Promise { 21 | router.get( 22 | "/states/:projectName", 23 | withIdentity, 24 | withParams, 25 | getStateHandler, 26 | ); 27 | router.post( 28 | "/states/:projectName", 29 | withIdentity, 30 | withParams, 31 | putStateHandler, 32 | ); 33 | router.delete( 34 | "/states/:projectName", 35 | withIdentity, 36 | withParams, 37 | deleteStateHandler, 38 | ); 39 | 40 | router.all( 41 | "/states/:projectName/lock", 42 | withIdentity, 43 | withParams, 44 | lockStateHandler, 45 | ); 46 | 47 | router.all("*", () => new Response("Not found.\n", { status: 404 })); 48 | return router.fetch(request, env); 49 | }, 50 | }; 51 | -------------------------------------------------------------------------------- /workers/tfstate/src/middlewares.ts: -------------------------------------------------------------------------------- 1 | import type { IRequest } from "itty-router"; 2 | import type { Env } from "./types/env"; 3 | import type { RequestWithIdentity } from "./types/request"; 4 | 5 | export function withParams(request: IRequest) { 6 | const { params } = request; 7 | request.projectName = params?.projectName; 8 | } 9 | 10 | export async function withIdentity( 11 | request: Request & RequestWithIdentity, 12 | env: Env, 13 | ): Promise { 14 | const authorization = request.headers.get("Authorization"); 15 | if (!authorization) 16 | return new Response("No authentication information provided.", { 17 | status: 401, 18 | }); 19 | 20 | const [basic, credentials] = authorization.split(" "); 21 | if (basic !== "Basic") 22 | return new Response("Only Basic authentication scheme is supported.", { 23 | status: 401, 24 | }); 25 | 26 | const [username, token] = Buffer.from(credentials, "base64") 27 | .toString() 28 | .split(":"); 29 | if (!username || username === "") 30 | return new Response("Username cannot be empty.", { status: 401 }); 31 | if (!token || token === "") 32 | return new Response("Password cannot be empty.", { status: 401 }); 33 | request.identity = { userInfo: { username } }; 34 | return undefined; 35 | } 36 | -------------------------------------------------------------------------------- /workers/tfstate/src/sha256.ts: -------------------------------------------------------------------------------- 1 | export function sha256(data: ArrayBuffer) { 2 | return crypto.subtle.digest("SHA-256", data); 3 | } 4 | 5 | export function buf2hex(buffer: ArrayBuffer): string { 6 | return [...new Uint8Array(buffer)] 7 | .map((x) => x.toString(16).padStart(2, "0")) 8 | .join(""); 9 | } 10 | -------------------------------------------------------------------------------- /workers/tfstate/src/types/env.d.ts: -------------------------------------------------------------------------------- 1 | export interface Env { 2 | // tfstate storage and locking 3 | TFSTATE_BUCKET: R2Bucket; 4 | TFSTATE_LOCK: DurableObjectNamespace; 5 | } 6 | -------------------------------------------------------------------------------- /workers/tfstate/src/types/request.d.ts: -------------------------------------------------------------------------------- 1 | import type { IRequest } from "itty-router"; 2 | 3 | export interface UserInfo { 4 | username: string; 5 | namespaceId?: string; 6 | } 7 | 8 | export interface RequestWithIdentity extends IRequest { 9 | identity?: { 10 | userInfo?: UserInfo; 11 | }; 12 | } 13 | -------------------------------------------------------------------------------- /workers/tfstate/src/types/terraform.d.ts: -------------------------------------------------------------------------------- 1 | // LockInfo 2 | // https://github.com/hashicorp/terraform/blob/cb340207d8840f3d2bc5dab100a5813d1ea3122b/internal/states/statemgr/locker.go#L115 3 | export interface LockInfo { 4 | ID: string; 5 | Operation: string; 6 | Info: string; 7 | Who: string; 8 | Version: string; 9 | Created: string; 10 | Path: string; 11 | } 12 | -------------------------------------------------------------------------------- /workers/tfstate/src/utils.ts: -------------------------------------------------------------------------------- 1 | export const getObjectKey = (email: string, projectName: string) => 2 | `${email}/${projectName}.tfstate`; 3 | -------------------------------------------------------------------------------- /workers/tfstate/test/env.d.ts: -------------------------------------------------------------------------------- 1 | import type { Env } from "../src/types/env"; 2 | 3 | declare module "cloudflare:test" { 4 | // Controls the type of `import("cloudflare:test").env` 5 | interface ProvidedEnv extends Env {} 6 | } 7 | -------------------------------------------------------------------------------- /workers/tfstate/test/index.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | createExecutionContext, 3 | env, 4 | waitOnExecutionContext, 5 | } from "cloudflare:test"; 6 | import { describe, expect, it } from "vitest"; 7 | // Could import any other source file/function here 8 | import worker from "../src"; 9 | 10 | describe("Echo worker", () => { 11 | it("responds with 200 OK", async () => { 12 | const request = new Request("http://example.com/states/hello", { 13 | headers: { Authorization: "Basic dml0ZXN0OnBhc3N3b3Jk" }, 14 | }); 15 | // Create an empty context to pass to `worker.fetch()` 16 | const ctx = createExecutionContext(); 17 | const response = await worker.fetch(request, env, ctx); 18 | // Wait for all `Promise`s passed to `ctx.waitUntil()` to settle before running test assertions 19 | await waitOnExecutionContext(ctx); 20 | expect(response.status).toBe(204); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /workers/tfstate/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json" 3 | } 4 | -------------------------------------------------------------------------------- /workers/tfstate/vitest.config.js: -------------------------------------------------------------------------------- 1 | import { defineWorkersConfig } from "@cloudflare/vitest-pool-workers/config"; 2 | 3 | export default defineWorkersConfig({ 4 | test: { 5 | poolOptions: { 6 | workers: { 7 | wrangler: { configPath: "./wrangler.toml" }, 8 | }, 9 | }, 10 | }, 11 | }); 12 | -------------------------------------------------------------------------------- /workers/tfstate/wrangler.toml: -------------------------------------------------------------------------------- 1 | name = "tfstate" 2 | account_id = "f5686db3c4f5b3e38b8f15b0561a28a8" 3 | compatibility_date = "2024-03-26" 4 | compatibility_flags = ["nodejs_compat"] 5 | 6 | main = "src/index.ts" 7 | 8 | routes = [{ pattern = "tfstate.mirio.dev", custom_domain = true }] 9 | 10 | r2_buckets = [ 11 | { binding = "TFSTATE_BUCKET", bucket_name = "tfstate-mirio-dev", preview_bucket_name = "tfstate-mirio-dev-preview" }, 12 | ] 13 | 14 | [durable_objects] 15 | bindings = [{ name = "TFSTATE_LOCK", class_name = "DurableLock" }] 16 | 17 | [[migrations]] 18 | tag = "v1" 19 | new_classes = ["DurableLock"] 20 | 21 | [build] 22 | command = "npm run build" 23 | --------------------------------------------------------------------------------