diff --git a/.github/workflows/static.yml b/.github/workflows/static.yml index 2f1ebef..3c50c83 100644 --- a/.github/workflows/static.yml +++ b/.github/workflows/static.yml @@ -33,7 +33,7 @@ jobs: - name: NodeJS Setup uses: actions/setup-node@v4 with: - node-version: latest + node-version: '20.x' cache: 'npm' cache-dependency-path: ts/sbd-server/package-lock.json diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b80d953..11ecb86 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -33,7 +33,7 @@ jobs: - name: NodeJS Setup uses: actions/setup-node@v4 with: - node-version: latest + node-version: '20.x' cache: 'npm' cache-dependency-path: ts/sbd-server/package-lock.json diff --git a/ts/sbd-server/README.md b/ts/sbd-server/README.md new file mode 100644 index 0000000..aee5bb1 --- /dev/null +++ b/ts/sbd-server/README.md @@ -0,0 +1,45 @@ +# Cloudflare SbdServer + +## Metrics + +It's hard to get a sense of what is going on with distributed durable objects. To mitigate this, we are writing metrics to a KV called `SBD_COORDINATION`. These metrics can be enumerated from a central metrics http endpoint. However, as these metrics list IP addresses, and cause billable load on our worker kv endpoint, they will be guarded by an api key. + +### Set Api Key + +Set these api keys via worker secrets in the form: `METRIC_API_$(whoami)="$(uuidgen)"`. + +E.g. `METRIC_API_NEONPHOG=d6d38e16-9fe2-4cbf-b66d-e49775064d59` + +If doing this through the cloudflare dashboard, don't forget to click `encrypt`! + +Or you can use `wrangler secret put METRIC_API_NEONPHOG` and then paste in the uuidgen result. + +### Access the Metrics Endpoint + +`$(url)/metris/$(whoami)/$(uuidgen)` + +E.g. `https://sbd.holo.host/metrics/NEONPHOG/d6d38e16-9fe2-4cbf-b66d-e49775064d59` + +Should give you something like: + +``` +# HELP client.count active client count +# TYPE client.count guage +client.count 4 + +# HELP client.recv.byte.count bytes received from client +# TYPE client.recv.byte.count guage +client.recv.byte.count{name="AqpAUD6QBb8lOfSBGna4C3tGAbI7E5slQ0MKkjm28kQ",opened=1720728911,active=1720728911,ip="no-ip"} 96 + +# HELP client.recv.byte.count bytes received from client +# TYPE client.recv.byte.count guage +client.recv.byte.count{name="R2qUfRVFfBhGGyxC4i_MorG48Ptk3JXwWvkIsh9mtRo",opened=1720728935,active=1720728935,ip="no-ip"} 96 + +# HELP client.recv.byte.count bytes received from client +# TYPE client.recv.byte.count guage +client.recv.byte.count{name="ia6EqgbobPhVrOdqlqZtL7v8EK5uDj5vlV4uTMB6vsY",opened=1720728936,active=1720728936,ip="no-ip"} 96 + +# HELP client.recv.byte.count bytes received from client +# TYPE client.recv.byte.count guage +client.recv.byte.count{name="u-JEHfSU6hArnHSjf9KA5W_ABH37go3Cm453UagScI8",opened=1720728911,active=1720728911,ip="no-ip"} 96 +``` diff --git a/ts/sbd-server/package-lock.json b/ts/sbd-server/package-lock.json index 697c100..2f760da 100644 --- a/ts/sbd-server/package-lock.json +++ b/ts/sbd-server/package-lock.json @@ -13,12 +13,13 @@ "js-base64": "^3.7.7" }, "devDependencies": { - "@cloudflare/workers-types": "^4.20240620.0", + "@cloudflare/vitest-pool-workers": "^0.4.11", + "@cloudflare/workers-types": "^4.20240712.0", "node-cleanup": "^2.1.2", "prettier": "^3.3.2", - "typescript": "^5.5.2", - "vitest": "^1.6.0", - "wrangler": "^3.63.1" + "typescript": "^5.5.3", + "vitest": "1.5.0", + "wrangler": "^3.64.0" } }, "node_modules/@cloudflare/kv-asset-handler": { @@ -33,10 +34,31 @@ "node": ">=16.13" } }, + "node_modules/@cloudflare/vitest-pool-workers": { + "version": "0.4.12", + "resolved": "https://registry.npmjs.org/@cloudflare/vitest-pool-workers/-/vitest-pool-workers-0.4.12.tgz", + "integrity": "sha512-i+ja4xJKPJv07DF1Q6zRGgRDHCMIJc39T+uXBU8DBaF+bjWPe+e589uTG27jCxVHuBvHxOiOv1KDt99EAZ/mpw==", + "dev": true, + "dependencies": { + "birpc": "0.2.14", + "cjs-module-lexer": "^1.2.3", + "devalue": "^4.3.0", + "esbuild": "0.17.19", + "miniflare": "3.20240712.0", + "semver": "^7.5.1", + "wrangler": "3.65.0", + "zod": "^3.22.3" + }, + "peerDependencies": { + "@vitest/runner": "1.3.x - 1.5.x", + "@vitest/snapshot": "1.3.x - 1.5.x", + "vitest": "1.3.x - 1.5.x" + } + }, "node_modules/@cloudflare/workerd-darwin-64": { - "version": "1.20240701.0", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20240701.0.tgz", - "integrity": "sha512-XAZa4ZP+qyTn6JQQACCPH09hGZXP2lTnWKkmg5mPwT8EyRzCKLkczAf98vPP5bq7JZD/zORdFWRY0dOTap8zTQ==", + "version": "1.20240712.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20240712.0.tgz", + "integrity": "sha512-KB1vbOhr62BCAwVr3VaRcngzPeSCQ7zPA9VGrfwYXIxo0Y4zlW1z0EVtcewFSz5XXKr3BtNnJXxdjDPUNkguQw==", "cpu": [ "x64" ], @@ -50,9 +72,9 @@ } }, "node_modules/@cloudflare/workerd-darwin-arm64": { - "version": "1.20240701.0", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20240701.0.tgz", - "integrity": "sha512-w80ZVAgfH4UwTz7fXZtk7KmS2FzlXniuQm4ku4+cIgRTilBAuKqjpOjwUCbx5g13Gqcm9NuiHce+IDGtobRTIQ==", + "version": "1.20240712.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20240712.0.tgz", + "integrity": "sha512-UDwFnCfQGFVCNxOeHxKNEc1ANQk/3OIiFWpVsxgZqJqU/22XM88JHxJW+YcBKsaUGUlpLyImaYUn2/rG+i+9UQ==", "cpu": [ "arm64" ], @@ -66,9 +88,9 @@ } }, "node_modules/@cloudflare/workerd-linux-64": { - "version": "1.20240701.0", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20240701.0.tgz", - "integrity": "sha512-UWLr/Anxwwe/25nGv451MNd2jhREmPt/ws17DJJqTLAx6JxwGWA15MeitAIzl0dbxRFAJa+0+R8ag2WR3F/D6g==", + "version": "1.20240712.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20240712.0.tgz", + "integrity": "sha512-MxpMHSJcZRUL66TO7BEnEim9WgZ8wJEVOB1Rq7a/IF2hI4/8f+N+02PChh62NkBlWxDfTXAtZy0tyQMm0EGjHg==", "cpu": [ "x64" ], @@ -82,9 +104,9 @@ } }, "node_modules/@cloudflare/workerd-linux-arm64": { - "version": "1.20240701.0", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20240701.0.tgz", - "integrity": "sha512-3kCnF9kYgov1ggpuWbgpXt4stPOIYtVmPCa7MO2xhhA0TWP6JDUHRUOsnmIgKrvDjXuXqlK16cdg3v+EWsaPJg==", + "version": "1.20240712.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20240712.0.tgz", + "integrity": "sha512-DtLYZsFFFAMgn+6YCHoQS6nYY4nbdAtcAFa4PhWTjLJDbvQEn3IoK9Bi4ajCL7xG36FeuBdZliSbBiiv7CJjfQ==", "cpu": [ "arm64" ], @@ -98,9 +120,9 @@ } }, "node_modules/@cloudflare/workerd-windows-64": { - "version": "1.20240701.0", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20240701.0.tgz", - "integrity": "sha512-6IPGITRAeS67j3BH1rN4iwYWDt47SqJG7KlZJ5bB4UaNAia4mvMBSy/p2p4vA89bbXoDRjMtEvRu7Robu6O7hQ==", + "version": "1.20240712.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20240712.0.tgz", + "integrity": "sha512-u8zoT9PQiiwxuz9npquLBFWrC/RlBWGGZ1aylarZNFlM4sFrRm+bRr6i+KtS+fltHIVXj3teuoKYytA1ppf9Yw==", "cpu": [ "x64" ], @@ -114,9 +136,9 @@ } }, "node_modules/@cloudflare/workers-types": { - "version": "4.20240620.0", - "resolved": "https://registry.npmjs.org/@cloudflare/workers-types/-/workers-types-4.20240620.0.tgz", - "integrity": "sha512-CQD8YS6evRob7LChvIX3gE3zYo0KVgaLDOu1SwNP1BVIS2Sa0b+FC8S1e1hhrNN8/E4chYlVN+FDAgA4KRDUEQ==", + "version": "4.20240712.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workers-types/-/workers-types-4.20240712.0.tgz", + "integrity": "sha512-C+C0ZnkRrxR2tPkZKAXwBsWEse7bWaA7iMbaG6IKaxaPTo/5ilx7Ei3BkI2izxmOJMsC05VS1eFUf95urXzhmw==", "dev": true }, "node_modules/@cspotcode/source-map-support": { @@ -170,9 +192,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", - "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.19.tgz", + "integrity": "sha512-rIKddzqhmav7MSmoFCmDIb6e2W57geRsM94gV2l38fzhXMwq7hZoClug9USI2pFRGL06f4IOPHHpFNOkWieR8A==", "cpu": [ "arm" ], @@ -186,9 +208,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", - "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.17.19.tgz", + "integrity": "sha512-KBMWvEZooR7+kzY0BtbTQn0OAYY7CsiydT63pVEaPtVYF0hXbUaOyZog37DKxK7NF3XacBJOpYT4adIJh+avxA==", "cpu": [ "arm64" ], @@ -202,9 +224,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", - "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.17.19.tgz", + "integrity": "sha512-uUTTc4xGNDT7YSArp/zbtmbhO0uEEK9/ETW29Wk1thYUJBz3IVnvgEiEwEa9IeLyvnpKrWK64Utw2bgUmDveww==", "cpu": [ "x64" ], @@ -218,9 +240,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", - "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.17.19.tgz", + "integrity": "sha512-80wEoCfF/hFKM6WE1FyBHc9SfUblloAWx6FJkFWTWiCoht9Mc0ARGEM47e67W9rI09YoUxJL68WHfDRYEAvOhg==", "cpu": [ "arm64" ], @@ -234,9 +256,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", - "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.17.19.tgz", + "integrity": "sha512-IJM4JJsLhRYr9xdtLytPLSH9k/oxR3boaUIYiHkAawtwNOXKE8KoU8tMvryogdcT8AU+Bflmh81Xn6Q0vTZbQw==", "cpu": [ "x64" ], @@ -250,9 +272,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", - "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.19.tgz", + "integrity": "sha512-pBwbc7DufluUeGdjSU5Si+P3SoMF5DQ/F/UmTSb8HXO80ZEAJmrykPyzo1IfNbAoaqw48YRpv8shwd1NoI0jcQ==", "cpu": [ "arm64" ], @@ -266,9 +288,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", - "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.17.19.tgz", + "integrity": "sha512-4lu+n8Wk0XlajEhbEffdy2xy53dpR06SlzvhGByyg36qJw6Kpfk7cp45DR/62aPH9mtJRmIyrXAS5UWBrJT6TQ==", "cpu": [ "x64" ], @@ -282,9 +304,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", - "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.17.19.tgz", + "integrity": "sha512-cdmT3KxjlOQ/gZ2cjfrQOtmhG4HJs6hhvm3mWSRDPtZ/lP5oe8FWceS10JaSJC13GBd4eH/haHnqf7hhGNLerA==", "cpu": [ "arm" ], @@ -298,9 +320,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", - "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.17.19.tgz", + "integrity": "sha512-ct1Tg3WGwd3P+oZYqic+YZF4snNl2bsnMKRkb3ozHmnM0dGWuxcPTTntAF6bOP0Sp4x0PjSF+4uHQ1xvxfRKqg==", "cpu": [ "arm64" ], @@ -314,9 +336,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", - "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.17.19.tgz", + "integrity": "sha512-w4IRhSy1VbsNxHRQpeGCHEmibqdTUx61Vc38APcsRbuVgK0OPEnQ0YD39Brymn96mOx48Y2laBQGqgZ0j9w6SQ==", "cpu": [ "ia32" ], @@ -330,9 +352,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", - "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.17.19.tgz", + "integrity": "sha512-2iAngUbBPMq439a+z//gE+9WBldoMp1s5GWsUSgqHLzLJ9WoZLZhpwWuym0u0u/4XmZ3gpHmzV84PonE+9IIdQ==", "cpu": [ "loong64" ], @@ -346,9 +368,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", - "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.17.19.tgz", + "integrity": "sha512-LKJltc4LVdMKHsrFe4MGNPp0hqDFA1Wpt3jE1gEyM3nKUvOiO//9PheZZHfYRfYl6AwdTH4aTcXSqBerX0ml4A==", "cpu": [ "mips64el" ], @@ -362,9 +384,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", - "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.17.19.tgz", + "integrity": "sha512-/c/DGybs95WXNS8y3Ti/ytqETiW7EU44MEKuCAcpPto3YjQbyK3IQVKfF6nbghD7EcLUGl0NbiL5Rt5DMhn5tg==", "cpu": [ "ppc64" ], @@ -378,9 +400,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", - "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.17.19.tgz", + "integrity": "sha512-FC3nUAWhvFoutlhAkgHf8f5HwFWUL6bYdvLc/TTuxKlvLi3+pPzdZiFKSWz/PF30TB1K19SuCxDTI5KcqASJqA==", "cpu": [ "riscv64" ], @@ -394,9 +416,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", - "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.17.19.tgz", + "integrity": "sha512-IbFsFbxMWLuKEbH+7sTkKzL6NJmG2vRyy6K7JJo55w+8xDk7RElYn6xvXtDW8HCfoKBFK69f3pgBJSUSQPr+4Q==", "cpu": [ "s390x" ], @@ -410,9 +432,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", - "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.17.19.tgz", + "integrity": "sha512-68ngA9lg2H6zkZcyp22tsVt38mlhWde8l3eJLWkyLrp4HwMUr3c1s/M2t7+kHIhvMjglIBrFpncX1SzMckomGw==", "cpu": [ "x64" ], @@ -426,9 +448,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", - "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.17.19.tgz", + "integrity": "sha512-CwFq42rXCR8TYIjIfpXCbRX0rp1jo6cPIUPSaWwzbVI4aOfX96OXY8M6KNmtPcg7QjYeDmN+DD0Wp3LaBOLf4Q==", "cpu": [ "x64" ], @@ -442,9 +464,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", - "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.17.19.tgz", + "integrity": "sha512-cnq5brJYrSZ2CF6c35eCmviIN3k3RczmHz8eYaVlNasVqsNY+JKohZU5MKmaOI+KkllCdzOKKdPs762VCPC20g==", "cpu": [ "x64" ], @@ -458,9 +480,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", - "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.17.19.tgz", + "integrity": "sha512-vCRT7yP3zX+bKWFeP/zdS6SqdWB8OIpaRq/mbXQxTGHnIxspRtigpkUcDMlSCOejlHowLqII7K2JKevwyRP2rg==", "cpu": [ "x64" ], @@ -474,9 +496,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", - "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.17.19.tgz", + "integrity": "sha512-yYx+8jwowUstVdorcMdNlzklLYhPxjniHWFKgRqH7IFlUEa0Umu3KuYplf1HUZZ422e3NU9F4LGb+4O0Kdcaag==", "cpu": [ "arm64" ], @@ -490,9 +512,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", - "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.17.19.tgz", + "integrity": "sha512-eggDKanJszUtCdlVs0RB+h35wNlb5v4TWEkq4vZcmVt5u/HiDZrTXe2bWFQUez3RgNHwx/x4sk5++4NSSicKkw==", "cpu": [ "ia32" ], @@ -506,9 +528,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", - "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.17.19.tgz", + "integrity": "sha512-lAhycmKnVOuRYNtRtatQR1LPQf2oYCkRGkSFnseDAKPl8lu5SOsK/e1sXe5a0Pc5kHIHe6P2I/ilntNv2xf3cA==", "cpu": [ "x64" ], @@ -552,9 +574,9 @@ } }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.15", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", - "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", "dev": true }, "node_modules/@jridgewell/trace-mapping": { @@ -587,9 +609,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.18.0.tgz", - "integrity": "sha512-Tya6xypR10giZV1XzxmH5wr25VcZSncG0pZIjfePT0OVBvqNEurzValetGNarVrGiq66EBVAFn15iYX4w6FKgQ==", + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.18.1.tgz", + "integrity": "sha512-lncuC4aHicncmbORnx+dUaAgzee9cm/PbIqgWz1PpXuwc+sa1Ct83tnqUDy/GFKleLiN7ZIeytM6KJ4cAn1SxA==", "cpu": [ "arm" ], @@ -600,9 +622,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.18.0.tgz", - "integrity": "sha512-avCea0RAP03lTsDhEyfy+hpfr85KfyTctMADqHVhLAF3MlIkq83CP8UfAHUssgXTYd+6er6PaAhx/QGv4L1EiA==", + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.18.1.tgz", + "integrity": "sha512-F/tkdw0WSs4ojqz5Ovrw5r9odqzFjb5LIgHdHZG65dFI1lWTWRVy32KDJLKRISHgJvqUeUhdIvy43fX41znyDg==", "cpu": [ "arm64" ], @@ -613,9 +635,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.18.0.tgz", - "integrity": "sha512-IWfdwU7KDSm07Ty0PuA/W2JYoZ4iTj3TUQjkVsO/6U+4I1jN5lcR71ZEvRh52sDOERdnNhhHU57UITXz5jC1/w==", + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.18.1.tgz", + "integrity": "sha512-vk+ma8iC1ebje/ahpxpnrfVQJibTMyHdWpOGZ3JpQ7Mgn/3QNHmPq7YwjZbIE7km73dH5M1e6MRRsnEBW7v5CQ==", "cpu": [ "arm64" ], @@ -626,9 +648,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.18.0.tgz", - "integrity": "sha512-n2LMsUz7Ynu7DoQrSQkBf8iNrjOGyPLrdSg802vk6XT3FtsgX6JbE8IHRvposskFm9SNxzkLYGSq9QdpLYpRNA==", + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.18.1.tgz", + "integrity": "sha512-IgpzXKauRe1Tafcej9STjSSuG0Ghu/xGYH+qG6JwsAUxXrnkvNHcq/NL6nz1+jzvWAnQkuAJ4uIwGB48K9OCGA==", "cpu": [ "x64" ], @@ -639,9 +661,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.18.0.tgz", - "integrity": "sha512-C/zbRYRXFjWvz9Z4haRxcTdnkPt1BtCkz+7RtBSuNmKzMzp3ZxdM28Mpccn6pt28/UWUCTXa+b0Mx1k3g6NOMA==", + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.18.1.tgz", + "integrity": "sha512-P9bSiAUnSSM7EmyRK+e5wgpqai86QOSv8BwvkGjLwYuOpaeomiZWifEos517CwbG+aZl1T4clSE1YqqH2JRs+g==", "cpu": [ "arm" ], @@ -652,9 +674,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.18.0.tgz", - "integrity": "sha512-l3m9ewPgjQSXrUMHg93vt0hYCGnrMOcUpTz6FLtbwljo2HluS4zTXFy2571YQbisTnfTKPZ01u/ukJdQTLGh9A==", + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.18.1.tgz", + "integrity": "sha512-5RnjpACoxtS+aWOI1dURKno11d7krfpGDEn19jI8BuWmSBbUC4ytIADfROM1FZrFhQPSoP+KEa3NlEScznBTyQ==", "cpu": [ "arm" ], @@ -665,9 +687,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.18.0.tgz", - "integrity": "sha512-rJ5D47d8WD7J+7STKdCUAgmQk49xuFrRi9pZkWoRD1UeSMakbcepWXPF8ycChBoAqs1pb2wzvbY6Q33WmN2ftw==", + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.18.1.tgz", + "integrity": "sha512-8mwmGD668m8WaGbthrEYZ9CBmPug2QPGWxhJxh/vCgBjro5o96gL04WLlg5BA233OCWLqERy4YUzX3bJGXaJgQ==", "cpu": [ "arm64" ], @@ -678,9 +700,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.18.0.tgz", - "integrity": "sha512-be6Yx37b24ZwxQ+wOQXXLZqpq4jTckJhtGlWGZs68TgdKXJgw54lUUoFYrg6Zs/kjzAQwEwYbp8JxZVzZLRepQ==", + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.18.1.tgz", + "integrity": "sha512-dJX9u4r4bqInMGOAQoGYdwDP8lQiisWb9et+T84l2WXk41yEej8v2iGKodmdKimT8cTAYt0jFb+UEBxnPkbXEQ==", "cpu": [ "arm64" ], @@ -691,9 +713,9 @@ ] }, "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.18.0.tgz", - "integrity": "sha512-hNVMQK+qrA9Todu9+wqrXOHxFiD5YmdEi3paj6vP02Kx1hjd2LLYR2eaN7DsEshg09+9uzWi2W18MJDlG0cxJA==", + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.18.1.tgz", + "integrity": "sha512-V72cXdTl4EI0x6FNmho4D502sy7ed+LuVW6Ym8aI6DRQ9hQZdp5sj0a2usYOlqvFBNKQnLQGwmYnujo2HvjCxQ==", "cpu": [ "ppc64" ], @@ -704,9 +726,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.18.0.tgz", - "integrity": "sha512-ROCM7i+m1NfdrsmvwSzoxp9HFtmKGHEqu5NNDiZWQtXLA8S5HBCkVvKAxJ8U+CVctHwV2Gb5VUaK7UAkzhDjlg==", + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.18.1.tgz", + "integrity": "sha512-f+pJih7sxoKmbjghrM2RkWo2WHUW8UbfxIQiWo5yeCaCM0TveMEuAzKJte4QskBp1TIinpnRcxkquY+4WuY/tg==", "cpu": [ "riscv64" ], @@ -717,9 +739,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.18.0.tgz", - "integrity": "sha512-0UyyRHyDN42QL+NbqevXIIUnKA47A+45WyasO+y2bGJ1mhQrfrtXUpTxCOrfxCR4esV3/RLYyucGVPiUsO8xjg==", + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.18.1.tgz", + "integrity": "sha512-qb1hMMT3Fr/Qz1OKovCuUM11MUNLUuHeBC2DPPAWUYYUAOFWaxInaTwTQmc7Fl5La7DShTEpmYwgdt2hG+4TEg==", "cpu": [ "s390x" ], @@ -730,9 +752,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.18.0.tgz", - "integrity": "sha512-xuglR2rBVHA5UsI8h8UbX4VJ470PtGCf5Vpswh7p2ukaqBGFTnsfzxUBetoWBWymHMxbIG0Cmx7Y9qDZzr648w==", + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.18.1.tgz", + "integrity": "sha512-7O5u/p6oKUFYjRbZkL2FLbwsyoJAjyeXHCU3O4ndvzg2OFO2GinFPSJFGbiwFDaCFc+k7gs9CF243PwdPQFh5g==", "cpu": [ "x64" ], @@ -743,9 +765,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.18.0.tgz", - "integrity": "sha512-LKaqQL9osY/ir2geuLVvRRs+utWUNilzdE90TpyoX0eNqPzWjRm14oMEE+YLve4k/NAqCdPkGYDaDF5Sw+xBfg==", + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.18.1.tgz", + "integrity": "sha512-pDLkYITdYrH/9Cv/Vlj8HppDuLMDUBmgsM0+N+xLtFd18aXgM9Nyqupb/Uw+HeidhfYg2lD6CXvz6CjoVOaKjQ==", "cpu": [ "x64" ], @@ -756,9 +778,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.18.0.tgz", - "integrity": "sha512-7J6TkZQFGo9qBKH0pk2cEVSRhJbL6MtfWxth7Y5YmZs57Pi+4x6c2dStAUvaQkHQLnEQv1jzBUW43GvZW8OFqA==", + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.18.1.tgz", + "integrity": "sha512-W2ZNI323O/8pJdBGil1oCauuCzmVd9lDmWBBqxYZcOqWD6aWqJtVBQ1dFrF4dYpZPks6F+xCZHfzG5hYlSHZ6g==", "cpu": [ "arm64" ], @@ -769,9 +791,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.18.0.tgz", - "integrity": "sha512-Txjh+IxBPbkUB9+SXZMpv+b/vnTEtFyfWZgJ6iyCmt2tdx0OF5WhFowLmnh8ENGNpfUlUZkdI//4IEmhwPieNg==", + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.18.1.tgz", + "integrity": "sha512-ELfEX1/+eGZYMaCIbK4jqLxO1gyTSOIlZr6pbC4SRYFaSIDVKOnZNMdoZ+ON0mrFDp4+H5MhwNC1H/AhE3zQLg==", "cpu": [ "ia32" ], @@ -782,9 +804,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.18.0.tgz", - "integrity": "sha512-UOo5FdvOL0+eIVTgS4tIdbW+TtnBLWg1YBCcU2KWM7nuNwRz9bksDX1bekJJCpu25N1DVWaCwnT39dVQxzqS8g==", + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.18.1.tgz", + "integrity": "sha512-yjk2MAkQmoaPYCSu35RLJ62+dz358nE83VfTePJRp8CG7aMg25mEJYpXFiD+NcevhX8LxD5OP5tktPXnXN7GDw==", "cpu": [ "x64" ], @@ -807,9 +829,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "20.14.9", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.9.tgz", - "integrity": "sha512-06OCtnTXtWOZBJlRApleWndH4JsRVs1pDCc8dLSQp+7PpUpX3ePdHyeNSFTeSe7FtKyQkrlPvHwJOW3SLd8Oyg==", + "version": "20.14.11", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.11.tgz", + "integrity": "sha512-kprQpL8MMeszbz6ojB5/tU8PLN4kesnN8Gjzw349rDlNgsSzg90lAVj3llK99Dh7JON+t9AuscPPFW6mPbTnSA==", "dev": true, "dependencies": { "undici-types": "~5.26.4" @@ -825,26 +847,42 @@ } }, "node_modules/@vitest/expect": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.6.0.tgz", - "integrity": "sha512-ixEvFVQjycy/oNgHjqsL6AZCDduC+tflRluaHIzKIsdbzkLn2U/iBnVeJwB6HsIjQBdfMR8Z0tRxKUsvFJEeWQ==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.5.0.tgz", + "integrity": "sha512-0pzuCI6KYi2SIC3LQezmxujU9RK/vwC1U9R0rLuGlNGcOuDWxqWKu6nUdFsX9tH1WU0SXtAxToOsEjeUn1s3hA==", "dev": true, "dependencies": { - "@vitest/spy": "1.6.0", - "@vitest/utils": "1.6.0", + "@vitest/spy": "1.5.0", + "@vitest/utils": "1.5.0", "chai": "^4.3.10" }, "funding": { "url": "https://opencollective.com/vitest" } }, + "node_modules/@vitest/expect/node_modules/@vitest/utils": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.5.0.tgz", + "integrity": "sha512-BDU0GNL8MWkRkSRdNFvCUCAVOeHaUlVJ9Tx0TYBZyXaaOTmGtUFObzchCivIBrIwKzvZA7A9sCejVhXM2aY98A==", + "dev": true, + "dependencies": { + "diff-sequences": "^29.6.3", + "estree-walker": "^3.0.3", + "loupe": "^2.3.7", + "pretty-format": "^29.7.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/@vitest/runner": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.6.0.tgz", - "integrity": "sha512-P4xgwPjwesuBiHisAVz/LSSZtDjOTPYZVmNAnpHHSR6ONrf8eCJOFRvUwdHn30F5M1fxhqtl7QZQUk2dprIXAg==", + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.5.3.tgz", + "integrity": "sha512-7PlfuReN8692IKQIdCxwir1AOaP5THfNkp0Uc4BKr2na+9lALNit7ub9l3/R7MP8aV61+mHKRGiqEKRIwu6iiQ==", "dev": true, + "peer": true, "dependencies": { - "@vitest/utils": "1.6.0", + "@vitest/utils": "1.5.3", "p-limit": "^5.0.0", "pathe": "^1.1.1" }, @@ -853,10 +891,11 @@ } }, "node_modules/@vitest/snapshot": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.6.0.tgz", - "integrity": "sha512-+Hx43f8Chus+DCmygqqfetcAZrDJwvTj0ymqjQq4CvmpKFSTVteEOBzCusu1x2tt4OJcvBflyHUE0DZSLgEMtQ==", + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.5.3.tgz", + "integrity": "sha512-K3mvIsjyKYBhNIDujMD2gfQEzddLe51nNOAf45yKRt/QFJcUIeTQd2trRvv6M6oCBHNVnZwFWbQ4yj96ibiDsA==", "dev": true, + "peer": true, "dependencies": { "magic-string": "^0.30.5", "pathe": "^1.1.1", @@ -867,9 +906,9 @@ } }, "node_modules/@vitest/spy": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.6.0.tgz", - "integrity": "sha512-leUTap6B/cqi/bQkXUu6bQV5TZPx7pmMBKBQiI0rJA8c3pB56ZsaTbREnF7CJfmvAS4V2cXIBAh/3rVwrrCYgw==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.5.0.tgz", + "integrity": "sha512-vu6vi6ew5N5MMHJjD5PoakMRKYdmIrNJmyfkhRpQt5d9Ewhw9nZ5Aqynbi3N61bvk9UvZ5UysMT6ayIrZ8GA9w==", "dev": true, "dependencies": { "tinyspy": "^2.2.0" @@ -879,10 +918,11 @@ } }, "node_modules/@vitest/utils": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.6.0.tgz", - "integrity": "sha512-21cPiuGMoMZwiOHa2i4LXkMkMkCGzA+MVFV70jRwHo95dL4x/ts5GZhML1QWuy7yfp3WzK3lRvZi3JnXTYqrBw==", + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.5.3.tgz", + "integrity": "sha512-rE9DTN1BRhzkzqNQO+kw8ZgfeEBCLXiHJwetk668shmNBpSagQxneT5eSqEBLP+cqSiAeecvQmbpFfdMyLcIQA==", "dev": true, + "peer": true, "dependencies": { "diff-sequences": "^29.6.3", "estree-walker": "^3.0.3", @@ -894,9 +934,9 @@ } }, "node_modules/acorn": { - "version": "8.12.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.0.tgz", - "integrity": "sha512-RTvkC4w+KNXrM39/lWCUaG0IbRkWdCv7W/IOW9oU6SawyxulvkQy5HQPVTKxEjczcUvapcrw3cFx/60VN/NRNw==", + "version": "8.12.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", + "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", "dev": true, "bin": { "acorn": "bin/acorn" @@ -972,6 +1012,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/birpc": { + "version": "0.2.14", + "resolved": "https://registry.npmjs.org/birpc/-/birpc-0.2.14.tgz", + "integrity": "sha512-37FHE8rqsYM5JEKCnXFyHpBCzvgHEExwVVTq+nUmloInU7l8ezD1TpOhKpS8oe1DTYFqEK27rFZVKG43oTqXRA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, "node_modules/blake3-wasm": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/blake3-wasm/-/blake3-wasm-2.1.5.tgz", @@ -1063,6 +1112,12 @@ "fsevents": "~2.3.2" } }, + "node_modules/cjs-module-lexer": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.3.1.tgz", + "integrity": "sha512-a3KdPAANPbNE4ZUv9h6LckSl9zLsYOP4MBmhIPkRaeyybt+r4UghLvq+xw/YwUcC1gqylCkL4rdVs3Lwupjm4Q==", + "dev": true + }, "node_modules/confbox": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.7.tgz", @@ -1152,6 +1207,12 @@ "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==", "dev": true }, + "node_modules/devalue": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/devalue/-/devalue-4.3.3.tgz", + "integrity": "sha512-UH8EL6H2ifcY8TbD2QsxwCC/pr5xSwPvv85LrLXVihmHVC3T3YqTCIwnR5ak0yO1KYqlxrPVOA/JVZJYPy2ATg==", + "dev": true + }, "node_modules/diff-sequences": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", @@ -1162,9 +1223,9 @@ } }, "node_modules/esbuild": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", - "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.17.19.tgz", + "integrity": "sha512-XQ0jAPFkK/u3LcVRcvVHQcTIqD6E2H1fvZMA5dQPSOWb3suUbWbfbRf94pjc0bNzRYLfIrDRQXr7X+LHIm5oHw==", "dev": true, "hasInstallScript": true, "bin": { @@ -1174,29 +1235,28 @@ "node": ">=12" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.21.5", - "@esbuild/android-arm": "0.21.5", - "@esbuild/android-arm64": "0.21.5", - "@esbuild/android-x64": "0.21.5", - "@esbuild/darwin-arm64": "0.21.5", - "@esbuild/darwin-x64": "0.21.5", - "@esbuild/freebsd-arm64": "0.21.5", - "@esbuild/freebsd-x64": "0.21.5", - "@esbuild/linux-arm": "0.21.5", - "@esbuild/linux-arm64": "0.21.5", - "@esbuild/linux-ia32": "0.21.5", - "@esbuild/linux-loong64": "0.21.5", - "@esbuild/linux-mips64el": "0.21.5", - "@esbuild/linux-ppc64": "0.21.5", - "@esbuild/linux-riscv64": "0.21.5", - "@esbuild/linux-s390x": "0.21.5", - "@esbuild/linux-x64": "0.21.5", - "@esbuild/netbsd-x64": "0.21.5", - "@esbuild/openbsd-x64": "0.21.5", - "@esbuild/sunos-x64": "0.21.5", - "@esbuild/win32-arm64": "0.21.5", - "@esbuild/win32-ia32": "0.21.5", - "@esbuild/win32-x64": "0.21.5" + "@esbuild/android-arm": "0.17.19", + "@esbuild/android-arm64": "0.17.19", + "@esbuild/android-x64": "0.17.19", + "@esbuild/darwin-arm64": "0.17.19", + "@esbuild/darwin-x64": "0.17.19", + "@esbuild/freebsd-arm64": "0.17.19", + "@esbuild/freebsd-x64": "0.17.19", + "@esbuild/linux-arm": "0.17.19", + "@esbuild/linux-arm64": "0.17.19", + "@esbuild/linux-ia32": "0.17.19", + "@esbuild/linux-loong64": "0.17.19", + "@esbuild/linux-mips64el": "0.17.19", + "@esbuild/linux-ppc64": "0.17.19", + "@esbuild/linux-riscv64": "0.17.19", + "@esbuild/linux-s390x": "0.17.19", + "@esbuild/linux-x64": "0.17.19", + "@esbuild/netbsd-x64": "0.17.19", + "@esbuild/openbsd-x64": "0.17.19", + "@esbuild/sunos-x64": "0.17.19", + "@esbuild/win32-arm64": "0.17.19", + "@esbuild/win32-ia32": "0.17.19", + "@esbuild/win32-x64": "0.17.19" } }, "node_modules/escape-string-regexp": { @@ -1373,9 +1433,9 @@ } }, "node_modules/is-core-module": { - "version": "2.14.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.14.0.tgz", - "integrity": "sha512-a5dFJih5ZLYlRtDc0dZWP7RiKr6xIKzmn/oAYCDvdLThadVgyJwlaoQPmRtMSpz+rk0OGAgIu+TcM9HUF0fk1A==", + "version": "2.15.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.0.tgz", + "integrity": "sha512-Dd+Lb2/zvk9SKy1TGCt1wFJFo/MWBPMX5x7KcvLajWTGuomczdQX61PvY5yK6SVACwpoexWo81IfFyoKY2QnTA==", "dev": true, "dependencies": { "hasown": "^2.0.2" @@ -1511,9 +1571,9 @@ } }, "node_modules/miniflare": { - "version": "3.20240701.0", - "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-3.20240701.0.tgz", - "integrity": "sha512-m9+I+7JNyqDGftCMKp9cK9pCZkK72hAL2mM9IWwhct+ZmucLBA8Uu6+rHQqA5iod86cpwOkrB2PrPA3wx9YNgw==", + "version": "3.20240712.0", + "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-3.20240712.0.tgz", + "integrity": "sha512-zVbsMX2phvJS1uTPmjK6CvVBq4ON2UkmvTw9IMfNPACsWJmHEdsBDxsYEG1vKAduJdI5gULLuJf7qpFxByDhGw==", "dev": true, "dependencies": { "@cspotcode/source-map-support": "0.8.1", @@ -1524,7 +1584,7 @@ "glob-to-regexp": "^0.4.1", "stoppable": "^1.1.0", "undici": "^5.28.4", - "workerd": "1.20240701.0", + "workerd": "1.20240712.0", "ws": "^8.17.1", "youch": "^3.2.2", "zod": "^3.22.3" @@ -1723,20 +1783,20 @@ } }, "node_modules/pkg-types": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.1.1.tgz", - "integrity": "sha512-ko14TjmDuQJ14zsotODv7dBlwxKhUKQEhuhmbqo1uCi9BB0Z2alo/wAXg6q1dTR5TyuqYyWhjtfe/Tsh+X28jQ==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.1.3.tgz", + "integrity": "sha512-+JrgthZG6m3ckicaOB74TwQ+tBWsFl3qVQg7mN8ulwSOElJ7gBhKzj2VkCPnZ4NlF6kEquYU+RIYNVAvzd54UA==", "dev": true, "dependencies": { "confbox": "^0.1.7", - "mlly": "^1.7.0", + "mlly": "^1.7.1", "pathe": "^1.1.2" } }, "node_modules/postcss": { - "version": "8.4.38", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz", - "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==", + "version": "8.4.39", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.39.tgz", + "integrity": "sha512-0vzE+lAiG7hZl1/9I8yzKLx3aR9Xbof3fBHKunvMfOCYAtMhrsnccJY2iTURb9EZd5+pLuiNV9/c/GZJOHsgIw==", "dev": true, "funding": [ { @@ -1754,7 +1814,7 @@ ], "dependencies": { "nanoid": "^3.3.7", - "picocolors": "^1.0.0", + "picocolors": "^1.0.1", "source-map-js": "^1.2.0" }, "engines": { @@ -1762,9 +1822,9 @@ } }, "node_modules/prettier": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.2.tgz", - "integrity": "sha512-rAVeHYMcv8ATV5d508CFdn+8/pHPpXeIid1DdrPwXnaAdH7cqjVbpJaT5eq4yRAFU/lsbwYwSF/n5iNrdJHPQA==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz", + "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==", "dev": true, "bin": { "prettier": "bin/prettier.cjs" @@ -1841,9 +1901,9 @@ } }, "node_modules/rollup": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.18.0.tgz", - "integrity": "sha512-QmJz14PX3rzbJCN1SG4Xe/bAAX2a6NpCP8ab2vfu2GiUr8AQcr2nCV/oEO3yneFarB67zk8ShlIyWb2LGTb3Sg==", + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.18.1.tgz", + "integrity": "sha512-Elx2UT8lzxxOXMpy5HWQGZqkrQOtrVDDa/bm9l10+U4rQnVzbL/LgZ4NOM1MPIDyHk69W4InuYDF5dzRh4Kw1A==", "dev": true, "dependencies": { "@types/estree": "1.0.5" @@ -1856,22 +1916,22 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.18.0", - "@rollup/rollup-android-arm64": "4.18.0", - "@rollup/rollup-darwin-arm64": "4.18.0", - "@rollup/rollup-darwin-x64": "4.18.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.18.0", - "@rollup/rollup-linux-arm-musleabihf": "4.18.0", - "@rollup/rollup-linux-arm64-gnu": "4.18.0", - "@rollup/rollup-linux-arm64-musl": "4.18.0", - "@rollup/rollup-linux-powerpc64le-gnu": "4.18.0", - "@rollup/rollup-linux-riscv64-gnu": "4.18.0", - "@rollup/rollup-linux-s390x-gnu": "4.18.0", - "@rollup/rollup-linux-x64-gnu": "4.18.0", - "@rollup/rollup-linux-x64-musl": "4.18.0", - "@rollup/rollup-win32-arm64-msvc": "4.18.0", - "@rollup/rollup-win32-ia32-msvc": "4.18.0", - "@rollup/rollup-win32-x64-msvc": "4.18.0", + "@rollup/rollup-android-arm-eabi": "4.18.1", + "@rollup/rollup-android-arm64": "4.18.1", + "@rollup/rollup-darwin-arm64": "4.18.1", + "@rollup/rollup-darwin-x64": "4.18.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.18.1", + "@rollup/rollup-linux-arm-musleabihf": "4.18.1", + "@rollup/rollup-linux-arm64-gnu": "4.18.1", + "@rollup/rollup-linux-arm64-musl": "4.18.1", + "@rollup/rollup-linux-powerpc64le-gnu": "4.18.1", + "@rollup/rollup-linux-riscv64-gnu": "4.18.1", + "@rollup/rollup-linux-s390x-gnu": "4.18.1", + "@rollup/rollup-linux-x64-gnu": "4.18.1", + "@rollup/rollup-linux-x64-musl": "4.18.1", + "@rollup/rollup-win32-arm64-msvc": "4.18.1", + "@rollup/rollup-win32-ia32-msvc": "4.18.1", + "@rollup/rollup-win32-x64-msvc": "4.18.1", "fsevents": "~2.3.2" } }, @@ -1939,6 +1999,18 @@ "node": ">=10" } }, + "node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -2123,9 +2195,9 @@ } }, "node_modules/typescript": { - "version": "5.5.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.2.tgz", - "integrity": "sha512-NcRtPEOsPFFWjobJEtfihkLCZCXZt/os3zf8nTxjVH3RvTSxjrCamJpbExGvYOF+tFHc3pA65qpdwPbzjohhew==", + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.3.tgz", + "integrity": "sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==", "dev": true, "bin": { "tsc": "bin/tsc", @@ -2136,9 +2208,9 @@ } }, "node_modules/ufo": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.5.3.tgz", - "integrity": "sha512-Y7HYmWaFwPUmkoQCUIAYpKqkOf+SbVj/2fJJZ4RJMCfZp0rTGwRbzQD+HghfnhKOjL9E01okqz+ncJskGYfBNw==", + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.5.4.tgz", + "integrity": "sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ==", "dev": true }, "node_modules/undici": { @@ -2175,13 +2247,13 @@ } }, "node_modules/vite": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.3.2.tgz", - "integrity": "sha512-6lA7OBHBlXUxiJxbO5aAY2fsHHzDr1q7DvXYnyZycRs2Dz+dXBWuhpWHvmljTRTpQC2uvGmUFFkSHF2vGo90MA==", + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.3.4.tgz", + "integrity": "sha512-Cw+7zL3ZG9/NZBB8C+8QbQZmR54GwqIz+WMI4b3JgdYJvX+ny9AjJXqkGQlDXSXRP9rP0B4tbciRMOVEKulVOA==", "dev": true, "dependencies": { "esbuild": "^0.21.3", - "postcss": "^8.4.38", + "postcss": "^8.4.39", "rollup": "^4.13.0" }, "bin": { @@ -2230,9 +2302,9 @@ } }, "node_modules/vite-node": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.6.0.tgz", - "integrity": "sha512-de6HJgzC+TFzOu0NTC4RAIsyf/DY/ibWDYQUcuEA84EMHhcefTUGkjFHKKEJhQN4A+6I0u++kr3l36ZF2d7XRw==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.5.0.tgz", + "integrity": "sha512-tV8h6gMj6vPzVCa7l+VGq9lwoJjW8Y79vst8QZZGiuRAfijU+EEWuc0kFpmndQrWhMMhet1jdSF+40KSZUqIIw==", "dev": true, "dependencies": { "cac": "^6.7.14", @@ -2251,184 +2323,26 @@ "url": "https://opencollective.com/vitest" } }, - "node_modules/vitest": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.6.0.tgz", - "integrity": "sha512-H5r/dN06swuFnzNFhq/dnz37bPXnq8xB2xB5JOVk8K09rUtoeNN+LHWkoQ0A/i3hvbUKKcCei9KpbxqHMLhLLA==", + "node_modules/vite/node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], "dev": true, - "dependencies": { - "@vitest/expect": "1.6.0", - "@vitest/runner": "1.6.0", - "@vitest/snapshot": "1.6.0", - "@vitest/spy": "1.6.0", - "@vitest/utils": "1.6.0", - "acorn-walk": "^8.3.2", - "chai": "^4.3.10", - "debug": "^4.3.4", - "execa": "^8.0.1", - "local-pkg": "^0.5.0", - "magic-string": "^0.30.5", - "pathe": "^1.1.1", - "picocolors": "^1.0.0", - "std-env": "^3.5.0", - "strip-literal": "^2.0.0", - "tinybench": "^2.5.1", - "tinypool": "^0.8.3", - "vite": "^5.0.0", - "vite-node": "1.6.0", - "why-is-node-running": "^2.2.2" - }, - "bin": { - "vitest": "vitest.mjs" - }, - "engines": { - "node": "^18.0.0 || >=20.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "@edge-runtime/vm": "*", - "@types/node": "^18.0.0 || >=20.0.0", - "@vitest/browser": "1.6.0", - "@vitest/ui": "1.6.0", - "happy-dom": "*", - "jsdom": "*" - }, - "peerDependenciesMeta": { - "@edge-runtime/vm": { - "optional": true - }, - "@types/node": { - "optional": true - }, - "@vitest/browser": { - "optional": true - }, - "@vitest/ui": { - "optional": true - }, - "happy-dom": { - "optional": true - }, - "jsdom": { - "optional": true - } - } - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/why-is-node-running": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.2.2.tgz", - "integrity": "sha512-6tSwToZxTOcotxHeA+qGCq1mVzKR3CwcJGmVcY+QE8SHy6TnpFnh8PAvPNHYr7EcuVeG0QSMxtYCuO1ta/G/oA==", - "dev": true, - "dependencies": { - "siginfo": "^2.0.0", - "stackback": "0.0.2" - }, - "bin": { - "why-is-node-running": "cli.js" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/workerd": { - "version": "1.20240701.0", - "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20240701.0.tgz", - "integrity": "sha512-qSgNVqauqzNCij9MaJLF2c2ko3AnFioVSIxMSryGbRK+LvtGr9BKBt6JOxCb24DoJASoJDx3pe3DJHBVydUiBg==", - "dev": true, - "hasInstallScript": true, - "bin": { - "workerd": "bin/workerd" - }, - "engines": { - "node": ">=16" - }, - "optionalDependencies": { - "@cloudflare/workerd-darwin-64": "1.20240701.0", - "@cloudflare/workerd-darwin-arm64": "1.20240701.0", - "@cloudflare/workerd-linux-64": "1.20240701.0", - "@cloudflare/workerd-linux-arm64": "1.20240701.0", - "@cloudflare/workerd-windows-64": "1.20240701.0" - } - }, - "node_modules/wrangler": { - "version": "3.63.1", - "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-3.63.1.tgz", - "integrity": "sha512-fxMPNEyDc9pZNtQOuYqRikzv6lL5eP4S1zv7L/kw24uu1cCEmJ39j8bfJGzrAEqKDNsiFXVjEka0RjlpgEVWPg==", - "dev": true, - "dependencies": { - "@cloudflare/kv-asset-handler": "0.3.4", - "@esbuild-plugins/node-globals-polyfill": "^0.2.3", - "@esbuild-plugins/node-modules-polyfill": "^0.2.2", - "blake3-wasm": "^2.1.5", - "chokidar": "^3.5.3", - "date-fns": "^3.6.0", - "esbuild": "0.17.19", - "miniflare": "3.20240701.0", - "nanoid": "^3.3.3", - "path-to-regexp": "^6.2.0", - "resolve": "^1.22.8", - "resolve.exports": "^2.0.2", - "selfsigned": "^2.0.1", - "source-map": "^0.6.1", - "unenv": "npm:unenv-nightly@1.10.0-1717606461.a117952", - "xxhash-wasm": "^1.0.1" - }, - "bin": { - "wrangler": "bin/wrangler.js", - "wrangler2": "bin/wrangler.js" - }, - "engines": { - "node": ">=16.17.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - }, - "peerDependencies": { - "@cloudflare/workers-types": "^4.20240620.0" - }, - "peerDependenciesMeta": { - "@cloudflare/workers-types": { - "optional": true - } - } - }, - "node_modules/wrangler/node_modules/@esbuild/android-arm": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.19.tgz", - "integrity": "sha512-rIKddzqhmav7MSmoFCmDIb6e2W57geRsM94gV2l38fzhXMwq7hZoClug9USI2pFRGL06f4IOPHHpFNOkWieR8A==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], + "optional": true, + "os": [ + "android" + ], "engines": { "node": ">=12" } }, - "node_modules/wrangler/node_modules/@esbuild/android-arm64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.17.19.tgz", - "integrity": "sha512-KBMWvEZooR7+kzY0BtbTQn0OAYY7CsiydT63pVEaPtVYF0hXbUaOyZog37DKxK7NF3XacBJOpYT4adIJh+avxA==", + "node_modules/vite/node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", "cpu": [ "arm64" ], @@ -2441,10 +2355,10 @@ "node": ">=12" } }, - "node_modules/wrangler/node_modules/@esbuild/android-x64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.17.19.tgz", - "integrity": "sha512-uUTTc4xGNDT7YSArp/zbtmbhO0uEEK9/ETW29Wk1thYUJBz3IVnvgEiEwEa9IeLyvnpKrWK64Utw2bgUmDveww==", + "node_modules/vite/node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", "cpu": [ "x64" ], @@ -2457,10 +2371,10 @@ "node": ">=12" } }, - "node_modules/wrangler/node_modules/@esbuild/darwin-arm64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.17.19.tgz", - "integrity": "sha512-80wEoCfF/hFKM6WE1FyBHc9SfUblloAWx6FJkFWTWiCoht9Mc0ARGEM47e67W9rI09YoUxJL68WHfDRYEAvOhg==", + "node_modules/vite/node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", "cpu": [ "arm64" ], @@ -2473,10 +2387,10 @@ "node": ">=12" } }, - "node_modules/wrangler/node_modules/@esbuild/darwin-x64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.17.19.tgz", - "integrity": "sha512-IJM4JJsLhRYr9xdtLytPLSH9k/oxR3boaUIYiHkAawtwNOXKE8KoU8tMvryogdcT8AU+Bflmh81Xn6Q0vTZbQw==", + "node_modules/vite/node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", "cpu": [ "x64" ], @@ -2489,10 +2403,10 @@ "node": ">=12" } }, - "node_modules/wrangler/node_modules/@esbuild/freebsd-arm64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.19.tgz", - "integrity": "sha512-pBwbc7DufluUeGdjSU5Si+P3SoMF5DQ/F/UmTSb8HXO80ZEAJmrykPyzo1IfNbAoaqw48YRpv8shwd1NoI0jcQ==", + "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", "cpu": [ "arm64" ], @@ -2505,10 +2419,10 @@ "node": ">=12" } }, - "node_modules/wrangler/node_modules/@esbuild/freebsd-x64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.17.19.tgz", - "integrity": "sha512-4lu+n8Wk0XlajEhbEffdy2xy53dpR06SlzvhGByyg36qJw6Kpfk7cp45DR/62aPH9mtJRmIyrXAS5UWBrJT6TQ==", + "node_modules/vite/node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", "cpu": [ "x64" ], @@ -2521,10 +2435,10 @@ "node": ">=12" } }, - "node_modules/wrangler/node_modules/@esbuild/linux-arm": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.17.19.tgz", - "integrity": "sha512-cdmT3KxjlOQ/gZ2cjfrQOtmhG4HJs6hhvm3mWSRDPtZ/lP5oe8FWceS10JaSJC13GBd4eH/haHnqf7hhGNLerA==", + "node_modules/vite/node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", "cpu": [ "arm" ], @@ -2537,10 +2451,10 @@ "node": ">=12" } }, - "node_modules/wrangler/node_modules/@esbuild/linux-arm64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.17.19.tgz", - "integrity": "sha512-ct1Tg3WGwd3P+oZYqic+YZF4snNl2bsnMKRkb3ozHmnM0dGWuxcPTTntAF6bOP0Sp4x0PjSF+4uHQ1xvxfRKqg==", + "node_modules/vite/node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", "cpu": [ "arm64" ], @@ -2553,10 +2467,10 @@ "node": ">=12" } }, - "node_modules/wrangler/node_modules/@esbuild/linux-ia32": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.17.19.tgz", - "integrity": "sha512-w4IRhSy1VbsNxHRQpeGCHEmibqdTUx61Vc38APcsRbuVgK0OPEnQ0YD39Brymn96mOx48Y2laBQGqgZ0j9w6SQ==", + "node_modules/vite/node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", "cpu": [ "ia32" ], @@ -2569,10 +2483,10 @@ "node": ">=12" } }, - "node_modules/wrangler/node_modules/@esbuild/linux-loong64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.17.19.tgz", - "integrity": "sha512-2iAngUbBPMq439a+z//gE+9WBldoMp1s5GWsUSgqHLzLJ9WoZLZhpwWuym0u0u/4XmZ3gpHmzV84PonE+9IIdQ==", + "node_modules/vite/node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", "cpu": [ "loong64" ], @@ -2585,10 +2499,10 @@ "node": ">=12" } }, - "node_modules/wrangler/node_modules/@esbuild/linux-mips64el": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.17.19.tgz", - "integrity": "sha512-LKJltc4LVdMKHsrFe4MGNPp0hqDFA1Wpt3jE1gEyM3nKUvOiO//9PheZZHfYRfYl6AwdTH4aTcXSqBerX0ml4A==", + "node_modules/vite/node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", "cpu": [ "mips64el" ], @@ -2601,10 +2515,10 @@ "node": ">=12" } }, - "node_modules/wrangler/node_modules/@esbuild/linux-ppc64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.17.19.tgz", - "integrity": "sha512-/c/DGybs95WXNS8y3Ti/ytqETiW7EU44MEKuCAcpPto3YjQbyK3IQVKfF6nbghD7EcLUGl0NbiL5Rt5DMhn5tg==", + "node_modules/vite/node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", "cpu": [ "ppc64" ], @@ -2617,10 +2531,10 @@ "node": ">=12" } }, - "node_modules/wrangler/node_modules/@esbuild/linux-riscv64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.17.19.tgz", - "integrity": "sha512-FC3nUAWhvFoutlhAkgHf8f5HwFWUL6bYdvLc/TTuxKlvLi3+pPzdZiFKSWz/PF30TB1K19SuCxDTI5KcqASJqA==", + "node_modules/vite/node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", "cpu": [ "riscv64" ], @@ -2633,10 +2547,10 @@ "node": ">=12" } }, - "node_modules/wrangler/node_modules/@esbuild/linux-s390x": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.17.19.tgz", - "integrity": "sha512-IbFsFbxMWLuKEbH+7sTkKzL6NJmG2vRyy6K7JJo55w+8xDk7RElYn6xvXtDW8HCfoKBFK69f3pgBJSUSQPr+4Q==", + "node_modules/vite/node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", "cpu": [ "s390x" ], @@ -2649,10 +2563,10 @@ "node": ">=12" } }, - "node_modules/wrangler/node_modules/@esbuild/linux-x64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.17.19.tgz", - "integrity": "sha512-68ngA9lg2H6zkZcyp22tsVt38mlhWde8l3eJLWkyLrp4HwMUr3c1s/M2t7+kHIhvMjglIBrFpncX1SzMckomGw==", + "node_modules/vite/node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", "cpu": [ "x64" ], @@ -2665,10 +2579,10 @@ "node": ">=12" } }, - "node_modules/wrangler/node_modules/@esbuild/netbsd-x64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.17.19.tgz", - "integrity": "sha512-CwFq42rXCR8TYIjIfpXCbRX0rp1jo6cPIUPSaWwzbVI4aOfX96OXY8M6KNmtPcg7QjYeDmN+DD0Wp3LaBOLf4Q==", + "node_modules/vite/node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", "cpu": [ "x64" ], @@ -2681,10 +2595,10 @@ "node": ">=12" } }, - "node_modules/wrangler/node_modules/@esbuild/openbsd-x64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.17.19.tgz", - "integrity": "sha512-cnq5brJYrSZ2CF6c35eCmviIN3k3RczmHz8eYaVlNasVqsNY+JKohZU5MKmaOI+KkllCdzOKKdPs762VCPC20g==", + "node_modules/vite/node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", "cpu": [ "x64" ], @@ -2697,10 +2611,10 @@ "node": ">=12" } }, - "node_modules/wrangler/node_modules/@esbuild/sunos-x64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.17.19.tgz", - "integrity": "sha512-vCRT7yP3zX+bKWFeP/zdS6SqdWB8OIpaRq/mbXQxTGHnIxspRtigpkUcDMlSCOejlHowLqII7K2JKevwyRP2rg==", + "node_modules/vite/node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", "cpu": [ "x64" ], @@ -2713,10 +2627,10 @@ "node": ">=12" } }, - "node_modules/wrangler/node_modules/@esbuild/win32-arm64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.17.19.tgz", - "integrity": "sha512-yYx+8jwowUstVdorcMdNlzklLYhPxjniHWFKgRqH7IFlUEa0Umu3KuYplf1HUZZ422e3NU9F4LGb+4O0Kdcaag==", + "node_modules/vite/node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", "cpu": [ "arm64" ], @@ -2729,10 +2643,10 @@ "node": ">=12" } }, - "node_modules/wrangler/node_modules/@esbuild/win32-ia32": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.17.19.tgz", - "integrity": "sha512-eggDKanJszUtCdlVs0RB+h35wNlb5v4TWEkq4vZcmVt5u/HiDZrTXe2bWFQUez3RgNHwx/x4sk5++4NSSicKkw==", + "node_modules/vite/node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", "cpu": [ "ia32" ], @@ -2745,10 +2659,10 @@ "node": ">=12" } }, - "node_modules/wrangler/node_modules/@esbuild/win32-x64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.17.19.tgz", - "integrity": "sha512-lAhycmKnVOuRYNtRtatQR1LPQf2oYCkRGkSFnseDAKPl8lu5SOsK/e1sXe5a0Pc5kHIHe6P2I/ilntNv2xf3cA==", + "node_modules/vite/node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", "cpu": [ "x64" ], @@ -2761,10 +2675,10 @@ "node": ">=12" } }, - "node_modules/wrangler/node_modules/esbuild": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.17.19.tgz", - "integrity": "sha512-XQ0jAPFkK/u3LcVRcvVHQcTIqD6E2H1fvZMA5dQPSOWb3suUbWbfbRf94pjc0bNzRYLfIrDRQXr7X+LHIm5oHw==", + "node_modules/vite/node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", "dev": true, "hasInstallScript": true, "bin": { @@ -2774,28 +2688,230 @@ "node": ">=12" }, "optionalDependencies": { - "@esbuild/android-arm": "0.17.19", - "@esbuild/android-arm64": "0.17.19", - "@esbuild/android-x64": "0.17.19", - "@esbuild/darwin-arm64": "0.17.19", - "@esbuild/darwin-x64": "0.17.19", - "@esbuild/freebsd-arm64": "0.17.19", - "@esbuild/freebsd-x64": "0.17.19", - "@esbuild/linux-arm": "0.17.19", - "@esbuild/linux-arm64": "0.17.19", - "@esbuild/linux-ia32": "0.17.19", - "@esbuild/linux-loong64": "0.17.19", - "@esbuild/linux-mips64el": "0.17.19", - "@esbuild/linux-ppc64": "0.17.19", - "@esbuild/linux-riscv64": "0.17.19", - "@esbuild/linux-s390x": "0.17.19", - "@esbuild/linux-x64": "0.17.19", - "@esbuild/netbsd-x64": "0.17.19", - "@esbuild/openbsd-x64": "0.17.19", - "@esbuild/sunos-x64": "0.17.19", - "@esbuild/win32-arm64": "0.17.19", - "@esbuild/win32-ia32": "0.17.19", - "@esbuild/win32-x64": "0.17.19" + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/vitest": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.5.0.tgz", + "integrity": "sha512-d8UKgR0m2kjdxDWX6911uwxout6GHS0XaGH1cksSIVVG8kRlE7G7aBw7myKQCvDI5dT4j7ZMa+l706BIORMDLw==", + "dev": true, + "dependencies": { + "@vitest/expect": "1.5.0", + "@vitest/runner": "1.5.0", + "@vitest/snapshot": "1.5.0", + "@vitest/spy": "1.5.0", + "@vitest/utils": "1.5.0", + "acorn-walk": "^8.3.2", + "chai": "^4.3.10", + "debug": "^4.3.4", + "execa": "^8.0.1", + "local-pkg": "^0.5.0", + "magic-string": "^0.30.5", + "pathe": "^1.1.1", + "picocolors": "^1.0.0", + "std-env": "^3.5.0", + "strip-literal": "^2.0.0", + "tinybench": "^2.5.1", + "tinypool": "^0.8.3", + "vite": "^5.0.0", + "vite-node": "1.5.0", + "why-is-node-running": "^2.2.2" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/node": "^18.0.0 || >=20.0.0", + "@vitest/browser": "1.5.0", + "@vitest/ui": "1.5.0", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/@vitest/runner": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.5.0.tgz", + "integrity": "sha512-7HWwdxXP5yDoe7DTpbif9l6ZmDwCzcSIK38kTSIt6CFEpMjX4EpCgT6wUmS0xTXqMI6E/ONmfgRKmaujpabjZQ==", + "dev": true, + "dependencies": { + "@vitest/utils": "1.5.0", + "p-limit": "^5.0.0", + "pathe": "^1.1.1" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest/node_modules/@vitest/snapshot": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.5.0.tgz", + "integrity": "sha512-qpv3fSEuNrhAO3FpH6YYRdaECnnRjg9VxbhdtPwPRnzSfHVXnNzzrpX4cJxqiwgRMo7uRMWDFBlsBq4Cr+rO3A==", + "dev": true, + "dependencies": { + "magic-string": "^0.30.5", + "pathe": "^1.1.1", + "pretty-format": "^29.7.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest/node_modules/@vitest/utils": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.5.0.tgz", + "integrity": "sha512-BDU0GNL8MWkRkSRdNFvCUCAVOeHaUlVJ9Tx0TYBZyXaaOTmGtUFObzchCivIBrIwKzvZA7A9sCejVhXM2aY98A==", + "dev": true, + "dependencies": { + "diff-sequences": "^29.6.3", + "estree-walker": "^3.0.3", + "loupe": "^2.3.7", + "pretty-format": "^29.7.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/workerd": { + "version": "1.20240712.0", + "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20240712.0.tgz", + "integrity": "sha512-hdIHZif82hBDy9YnMtcmDGgbLU5f2P2aGpi/X8EKhTSLDppVUGrkY3XB536J4jGjA2D5dS0FUEXCl5bAJEed8Q==", + "dev": true, + "hasInstallScript": true, + "bin": { + "workerd": "bin/workerd" + }, + "engines": { + "node": ">=16" + }, + "optionalDependencies": { + "@cloudflare/workerd-darwin-64": "1.20240712.0", + "@cloudflare/workerd-darwin-arm64": "1.20240712.0", + "@cloudflare/workerd-linux-64": "1.20240712.0", + "@cloudflare/workerd-linux-arm64": "1.20240712.0", + "@cloudflare/workerd-windows-64": "1.20240712.0" + } + }, + "node_modules/wrangler": { + "version": "3.65.0", + "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-3.65.0.tgz", + "integrity": "sha512-IDy4ttyJZssazAd5CXHw4NWeZFGxngdNF5m2ogltdT3CV7uHfCvPVdMcr4uNMpRZd0toHmAE3LtQeXxDFFp88A==", + "dev": true, + "dependencies": { + "@cloudflare/kv-asset-handler": "0.3.4", + "@esbuild-plugins/node-globals-polyfill": "^0.2.3", + "@esbuild-plugins/node-modules-polyfill": "^0.2.2", + "blake3-wasm": "^2.1.5", + "chokidar": "^3.5.3", + "date-fns": "^3.6.0", + "esbuild": "0.17.19", + "miniflare": "3.20240712.0", + "nanoid": "^3.3.3", + "path-to-regexp": "^6.2.0", + "resolve": "^1.22.8", + "resolve.exports": "^2.0.2", + "selfsigned": "^2.0.1", + "source-map": "^0.6.1", + "unenv": "npm:unenv-nightly@1.10.0-1717606461.a117952", + "xxhash-wasm": "^1.0.1" + }, + "bin": { + "wrangler": "bin/wrangler.js", + "wrangler2": "bin/wrangler.js" + }, + "engines": { + "node": ">=16.17.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + }, + "peerDependencies": { + "@cloudflare/workers-types": "^4.20240712.0" + }, + "peerDependenciesMeta": { + "@cloudflare/workers-types": { + "optional": true + } } }, "node_modules/ws": { @@ -2826,9 +2942,9 @@ "dev": true }, "node_modules/yocto-queue": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.0.0.tgz", - "integrity": "sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.1.1.tgz", + "integrity": "sha512-b4JR1PFR10y1mKjhHY9LaGo6tmrgjit7hxVIeAmyMw3jegXR4dhYqLaQF5zMXZxY7tLpMyJeLjr1C4rLmkVe8g==", "dev": true, "engines": { "node": ">=12.20" diff --git a/ts/sbd-server/package.json b/ts/sbd-server/package.json index e54f0fb..b3857b2 100644 --- a/ts/sbd-server/package.json +++ b/ts/sbd-server/package.json @@ -2,22 +2,24 @@ "name": "sbd-signal", "version": "0.0.0", "private": true, + "type": "module", "scripts": { - "deploy": "wrangler deploy --minify", + "deploy": "wrangler deploy", "dev": "wrangler dev", "start": "wrangler dev", - "test:fmt": "prettier -w src", - "test:type": "tsc --noEmit", + "test:fmt": "prettier -w src test", + "test:type": "tsc --noEmit --project ./src/tsconfig.json", "test:unit": "vitest run", "test": "npm run test:fmt && npm run test:type && npm run test:unit" }, "devDependencies": { - "@cloudflare/workers-types": "^4.20240620.0", + "@cloudflare/vitest-pool-workers": "^0.4.11", + "@cloudflare/workers-types": "^4.20240712.0", "node-cleanup": "^2.1.2", "prettier": "^3.3.2", - "typescript": "^5.5.2", - "vitest": "^1.6.0", - "wrangler": "^3.63.1" + "typescript": "^5.5.3", + "vitest": "1.5.0", + "wrangler": "^3.64.0" }, "dependencies": { "@noble/ed25519": "^2.1.0", diff --git a/ts/sbd-server/src/b64.ts b/ts/sbd-server/src/b64.ts deleted file mode 100644 index 585d33f..0000000 --- a/ts/sbd-server/src/b64.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { fromUint8Array, toUint8Array } from 'js-base64'; - -/** - * Convert to base64url representation. - */ -export function toB64Url(s: Uint8Array): string { - return fromUint8Array(s) - .replace(/\=/g, '') - .replace(/\+/g, '-') - .replace(/\//g, '_'); -} - -/** - * Convert from base64url representation. - */ -export function fromB64Url(s: string): Uint8Array { - return toUint8Array(s.replace(/\-/g, '+').replace(/\_/g, '/')); -} diff --git a/ts/sbd-server/src/common.ts b/ts/sbd-server/src/common.ts new file mode 100644 index 0000000..126de58 --- /dev/null +++ b/ts/sbd-server/src/common.ts @@ -0,0 +1,108 @@ +import { fromUint8Array, toUint8Array } from 'js-base64'; +import * as ed from '@noble/ed25519'; +import { sha512 } from '@noble/hashes/sha512'; +ed.etc.sha512Sync = (...m) => sha512(ed.etc.concatBytes(...m)); +export { ed }; + +/** + * How long to wait ahead of "now" to batch up message sends. + * Note, we're setting this to zero which will try to send queued messages + * as fast as possible. This doesn't mean messages won't be queued/batched, + * since there will be a delay between requesting an alarm and when it + * is actually invoked + however long it takes to actually run. + */ +export const BATCH_DUR_MS = 0; + +/** + * How many nanoseconds of rate limiting quota should be burned by a single + * byte sent through the system. Higher numbers mean slower rate limiting. + */ +export const LIMIT_NANOS_PER_BYTE = 8000; + +/** + * How many nanoseconds of rate limiting burst graceperiod is allowed + */ +export const LIMIT_NANOS_BURST = LIMIT_NANOS_PER_BYTE * 16 * 16 * 1024; + +/** + * Milliseconds connections are allowed to remain idle before being closed. + */ +export const LIMIT_IDLE_MILLIS = 10000; + +/** + * Max message size. + */ +export const MAX_MESSAGE_BYTES = 20000; + +/** + * Mixin to allow errors with status codes. + */ +export interface AddStatus { + status: number; +} + +/** + * Error type with a status code. + */ +export type StatusError = Error & AddStatus; + +/** + * Adds a 'status' property to ts Error type. + * If not specified will be set to 500. + * Allows altering the http or ws error status for responses. + * In the case of a websocket error, the http status code + * will be added to 4000 for user-specified error codes. + */ +export function err(e: string, s?: number): StatusError { + const out: any = new Error(e); + out.status = s || 500; + return out; +} + +/** + * Seconds since epoch timestamp. + */ +export function timestamp(): number { + return (Date.now() / 1000) | 0; +} + +/** + * Pull pubKey string and bytes from the url path. + */ +export function parsePubKey(path: string): { + pubKeyStr: string; + pubKeyBytes: Uint8Array; +} { + const parts: Array = path.split('/'); + + if (parts.length !== 2) { + throw err('expected single pubKey item on path', 400); + } + + const pubKeyStr = parts[1]; + + const pubKeyBytes = fromB64Url(parts[1]); + + if (pubKeyBytes.length !== 32) { + throw err('invalid pubKey length', 400); + } + + return { pubKeyStr, pubKeyBytes }; +} + +/** + * Convert to base64url representation. + */ +export function toB64Url(s: Uint8Array): string { + return fromUint8Array(s) + .replace(/\=/g, '') + .replace(/\+/g, '-') + .replace(/\//g, '_'); +} + +/** + * Convert from base64url representation. + */ +export function fromB64Url(s: string): Uint8Array { + return toUint8Array(s.replace(/\-/g, '+').replace(/\_/g, '/')); +} diff --git a/ts/sbd-server/src/ed.ts b/ts/sbd-server/src/ed.ts deleted file mode 100644 index 5436b54..0000000 --- a/ts/sbd-server/src/ed.ts +++ /dev/null @@ -1,7 +0,0 @@ -// This ed25519 lib annoyingly needs a hash lib explicitly configured. -// Do that with their own recommended hash lib, and re-export that. - -import * as ed from '@noble/ed25519'; -import { sha512 } from '@noble/hashes/sha512'; -ed.etc.sha512Sync = (...m) => sha512(ed.etc.concatBytes(...m)); -export { ed }; diff --git a/ts/sbd-server/src/env.d.ts b/ts/sbd-server/src/env.d.ts new file mode 100644 index 0000000..692a6ab --- /dev/null +++ b/ts/sbd-server/src/env.d.ts @@ -0,0 +1,20 @@ +/** + * Cloudflare worker environment objects. + */ +interface EnvExplicit { + SBD_COORDINATION: KVNamespace; + SIGNAL: DurableObjectNamespace; + RATE_LIMIT: DurableObjectNamespace; +} + +/** + * Cloudflare worker environment variables. + */ +interface EnvVars { + [index: string]: string; +} + +/** + * Combined Cloudflare Env type. + */ +type Env = EnvExplicit & EnvVars; diff --git a/ts/sbd-server/src/err.ts b/ts/sbd-server/src/err.ts deleted file mode 100644 index f7b8feb..0000000 --- a/ts/sbd-server/src/err.ts +++ /dev/null @@ -1,18 +0,0 @@ -interface AddStatus { - status: number; -} - -type StatusError = Error & AddStatus; - -/** - * Adds a 'status' property to ts Error type. - * If not specified will be set to 500. - * Allows altering the http or ws error status for responses. - * In the case of a websocket error, the http status code - * will be added to 4000 for user-specified error codes. - */ -export function err(e: string, s?: number): StatusError { - const out: any = new Error(e); - out.status = s || 500; - return out; -} diff --git a/ts/sbd-server/src/index.ts b/ts/sbd-server/src/index.ts index 773829c..1283276 100644 --- a/ts/sbd-server/src/index.ts +++ b/ts/sbd-server/src/index.ts @@ -1,76 +1,14 @@ import { DurableObject } from 'cloudflare:workers'; -import { RateLimit, RateLimitResult } from './rate-limit.ts'; -import { err } from './err.ts'; -import { ed } from './ed.ts'; -import { toB64Url, fromB64Url } from './b64.ts'; -import { - Msg, - MsgLbrt, - MsgLidl, - MsgAreq, - MsgAres, - MsgSrdy, - MsgKeep, - MsgNone, - MsgForward, -} from './msg.ts'; -/** - * How long to wait ahead of "now" to batch up message sends. - * Note, we're setting this to zero which will try to send queued messages - * as fast as possible. This doesn't mean messages won't be queued/batched, - * since there will be a delay between requesting an alarm and when it - * is actually invoked + however long it takes to actually run. - */ -const BATCH_DUR_MS = 0; +import * as common from './common.ts'; -/** - * How many nanoseconds of rate limiting quota should be burned by a single - * byte sent through the system. Higher numbers mean slower rate limiting. - */ -const LIMIT_NANOS_PER_BYTE = 8000; +import { Prom } from './prom.ts'; -/** - * Milliseconds connections are allowed to remain idle before being closed. - */ -const LIMIT_IDLE_MILLIS = 10000; +import { DoRateLimit } from './rate-limit.ts'; +export { DoRateLimit }; -/** - * Max message size. - */ -const MAX_MESSAGE_BYTES = 20000; - -/** - * Cloudflare worker environment objects. - */ -export interface Env { - SIGNAL: DurableObjectNamespace; - RATE_LIMIT: DurableObjectNamespace; -} - -/** - * Pull pubKey string and bytes from the url path. - */ -function parsePubKey(path: string): { - pubKeyStr: string; - pubKeyBytes: Uint8Array; -} { - const parts: Array = path.split('/'); - - if (parts.length !== 2) { - throw err('expected single pubKey item on path', 400); - } - - const pubKeyStr = parts[1]; - - const pubKeyBytes = fromB64Url(parts[1]); - - if (pubKeyBytes.length !== 32) { - throw err('invalid pubKey length', 400); - } - - return { pubKeyStr, pubKeyBytes }; -} +import { DoSignal } from './signal.ts'; +export { DoSignal }; /** * This is the http entrypoint. @@ -91,20 +29,103 @@ export default { // TODO - check headers for content-length / chunked encoding and reject? if (method !== 'GET') { - throw err('expected GET', 400); + throw common.err('expected GET', 400); } - const { pubKeyStr } = parsePubKey(url.pathname); + let pathParts = url.pathname.split('/'); + + if (pathParts.length === 4 && pathParts[1] === 'metrics') { + if (env['METRICS_API_' + pathParts[2]] !== pathParts[3]) { + throw common.err('bad metrics api key', 400); + } + + const p = new Prom(); + + let res = await env.SBD_COORDINATION.list({ prefix: 'client' }); + + let count = 0; + + while (true) { + for (const item of res.keys) { + if ( + 'name' in item && + item.metadata && + typeof item.metadata === 'object' + ) { + count += 1; + + let opened = 0; + let active = 0; + let activeBytesReceived = 0; + + if ( + 'op' in item.metadata && + typeof item.metadata.op === 'number' + ) { + opened = item.metadata.op; + } + if ( + 'ac' in item.metadata && + typeof item.metadata.ac === 'number' + ) { + active = item.metadata.ac; + } + if ( + 'br' in item.metadata && + typeof item.metadata.br === 'number' + ) { + activeBytesReceived = item.metadata.br; + } + + const now = common.timestamp(); + + const openedD = now - opened; + const activeD = now - active; + + p.guage( + false, + 'client.recv.byte.count', + 'bytes received from client', + { + name: item.name.split(':')[1], + opened, + openedD, + active, + activeD, + ip, + }, + activeBytesReceived, + ); + } + } + + if (res.list_complete) { + break; + } + + res = await env.SBD_COORDINATION.list({ + prefix: 'client', + cursor: res.cursor, + }); + } + + p.guage(true, 'client.count', 'active client count', {}, count); + + return new Response(await p.render()); + } + + const { pubKeyStr } = common.parsePubKey(url.pathname); const ipId = env.RATE_LIMIT.idFromName(ip); const ipStub = env.RATE_LIMIT.get(ipId) as DurableObjectStub; const { shouldBlock } = await ipStub.bytesReceived( Date.now(), + ip, pubKeyStr, 1, ); if (shouldBlock) { - throw err(`limit`, 429); + throw common.err(`limit`, 429); } // DO instanced by our pubKey @@ -121,326 +142,3 @@ export default { } }, }; - -/** - * "RATE_LIMIT" durable object. - * This is a thin wrapper around the "RateLimit" class. - */ -export class DoRateLimit extends DurableObject { - ctx: DurableObjectState; - env: Env; - rl: RateLimit; - - constructor(ctx: DurableObjectState, env: Env) { - super(ctx, env); - this.ctx = ctx; - this.env = env; - this.rl = new RateLimit(LIMIT_NANOS_PER_BYTE, 16 * 16 * 1024); - } - - async bytesReceived( - now: number, - pk: string, - bytes: number, - ): Promise { - return this.rl.bytesReceived(now, pk, bytes); - } -} - -/** - * "SIGNAL" durable object. - */ -export class DoSignal extends DurableObject { - ctx: DurableObjectState; - env: Env; - queue: { [index: string]: Array }; - alarmLock: boolean; - curLimit: number; - - constructor(ctx: DurableObjectState, env: Env) { - super(ctx, env); - this.ctx = ctx; - this.env = env; - this.queue = {}; - this.alarmLock = false; - this.curLimit = 0; - } - - /** - * Client websockets are connected to a durable object identified - * by their own pubKey. When they send forward messages, those must - * be sent to durable objects identified by the destination pubKey. - * This is the api for those messages to be forwarded. - */ - async forward(messageList: Array) { - for (const ws of this.ctx.getWebSockets()) { - for (const message of messageList) { - ws.send(message); - } - } - } - - /** - * This is the http endpoint for the "SIGNAL" durable object. - * The worker http fetch endpoint above forwards the request here. - * This function performs some checks, then upgrades the connection - * to a websocket. - */ - async fetch(request: Request): Promise { - let cleanServer = null; - try { - const ip = request.headers.get('cf-connecting-ip') || 'no-ip'; - const url = new URL(request.url); - - const { pubKeyStr, pubKeyBytes } = parsePubKey(url.pathname); - - if (this.ctx.getWebSockets().length > 0) { - throw err('websocket already connected', 400); - } - if (request.headers.get('Upgrade') !== 'websocket') { - throw err('expected websocket', 426); - } - - const [client, server] = Object.values(new WebSocketPair()); - - this.ctx.acceptWebSocket(server); - cleanServer = server; - - const nonce = new Uint8Array(32); - crypto.getRandomValues(nonce); - - server.serializeAttachment({ - pubKey: pubKeyBytes, - ip, - nonce, - valid: false, - }); - - // this will also send MsgLbrt - await this.ipRateLimit(ip, pubKeyStr, 1, server); - - server.send(new MsgLidl(LIMIT_IDLE_MILLIS).encoded()); - server.send(new MsgAreq(nonce).encoded()); - - console.log( - 'webSocketOpen', - JSON.stringify({ - pubKey: pubKeyStr, - ip, - nonce: toB64Url(nonce), - }), - ); - - return new Response(null, { status: 101, webSocket: client }); - } catch (e: any) { - console.error('error', e.toString()); - if (cleanServer) { - cleanServer.close(4000 + (e.status || 500), e.toString()); - } - return new Response(JSON.stringify({ err: e.toString() }), { - status: e.status || 500, - }); - } - } - - /** - * Helper function for performing the rate-limit check and - * closing the websocket if we violate the limit. - */ - async ipRateLimit(ip: string, pk: string, bytes: number, ws: WebSocket) { - try { - const ipId = this.env.RATE_LIMIT.idFromName(ip); - const ipStub = this.env.RATE_LIMIT.get( - ipId, - ) as DurableObjectStub; - const { limitNanosPerByte, shouldBlock } = await ipStub.bytesReceived( - Date.now(), - pk, - bytes, - ); - if (shouldBlock) { - throw err(`limit`, 429); - } - if (this.curLimit !== limitNanosPerByte) { - this.curLimit = limitNanosPerByte; - ws.send(new MsgLbrt(limitNanosPerByte).encoded()); - } - } catch (e) { - throw err(`limit ${e}`, 429); - } - } - - /** - * Handle incoming websocket messages. - * First handshake the connection, then start handling forwarding messages. - */ - async webSocketMessage(ws: WebSocket, message: ArrayBuffer | string) { - await this.ctx.blockConcurrencyWhile(async () => { - try { - const { pubKey, ip, nonce, valid } = ws.deserializeAttachment(); - if (!pubKey) { - throw err('no associated pubKey'); - } - if (!ip) { - throw err('no associated ip'); - } - - // convert strings into binary - let msgRaw: Uint8Array; - if (message instanceof ArrayBuffer) { - msgRaw = new Uint8Array(message); - } else { - const enc = new TextEncoder(); - msgRaw = enc.encode(message); - } - - await this.ipRateLimit(ip, pubKey, msgRaw.byteLength, ws); - - if (msgRaw.byteLength > MAX_MESSAGE_BYTES) { - throw err('max message length exceeded', 400); - } - - const msg = Msg.parse(msgRaw); - - if (!valid) { - if (!nonce) { - throw err('no associated nonce'); - } - - if (msg instanceof MsgAres) { - if (!ed.verify(msg.signature(), nonce, pubKey)) { - throw err('invalid handshake signature', 400); - } - - ws.send(new MsgSrdy().encoded()); - - ws.serializeAttachment({ - pubKey, - ip, - nonce: true, // don't need to keep the actual nonce anymore - valid: true, - }); - - console.log( - 'webSocketAuthenticated', - JSON.stringify({ pubKey: toB64Url(pubKey) }), - ); - } else { - if (msg instanceof MsgForward) { - throw err(`invalid forward before handshake`); - } - // otherwise just ignore the message - } - } else { - if (msg instanceof MsgNone) { - // no-op - } else if (msg instanceof MsgKeep) { - // keep alive - } else if (msg instanceof MsgForward) { - // extract the destination pubKey (slice does a copy) - const dest = msg.pubKey().slice(0); - - // overwrite the destination pubKey with the source (our) pubKey - // the pubKey() function returns a reference, so editing it - // alters the message that will be sent - msg.pubKey().set(pubKey, 0); - - const id = toB64Url(dest); - if (!this.queue[id]) { - this.queue[id] = []; - } - this.queue[id].push(msg.encoded()); - - if (!this.alarmLock) { - const alarm = await this.ctx.storage.getAlarm(); - if (!alarm) { - this.ctx.storage.setAlarm(Date.now() + BATCH_DUR_MS); - } - } - } else { - throw err(`invalid post-handshake message type: ${msg.type()}`); - } - } - } catch (e: any) { - console.error('error', e.toString()); - ws.close(4000 + (e.status || 500), e.toString()); - } - }); - } - - /** - * The `webSocketMessage` handler above enqueues messages for delivery, - * then sets up an alarm to handle actually forwarding them. This ensures - * the messages are delivered in order without deadlocking two clients - * that happened to try to forward messages to each other at the same moment. - */ - async alarm() { - const { shouldReturn, queue } = await this.ctx.blockConcurrencyWhile( - async () => { - if (this.alarmLock || Object.keys(this.queue).length === 0) { - return { shouldReturn: true, queue: {} }; - } - this.alarmLock = true; - const queue = this.queue; - this.queue = {}; - return { shouldReturn: false, queue }; - }, - ); - - if (shouldReturn) { - return; - } - - // We cannot do the actual forwarding within a blockConcurrency because - // then if two peers try to send each other data at the same time it - // will deadlock. Hence all the complexity with the alarms and alarmLock. - - for (const idName in queue) { - try { - const id = this.env.SIGNAL.idFromName(idName); - const stub = this.env.SIGNAL.get(id) as DurableObjectStub; - - await stub.forward(queue[idName]); - } catch (_e: any) { - // It is okay to get errors forwarding to peers, they may have - // disconnected. We still want to forward to other peers who - // may still be there. - } - } - - await this.ctx.blockConcurrencyWhile(async () => { - this.alarmLock = false; - - if (Object.keys(this.queue).length !== 0) { - const alarm = await this.ctx.storage.getAlarm(); - if (!alarm) { - this.ctx.storage.setAlarm(Date.now() + BATCH_DUR_MS); - } - } - }); - } - - /** - * The websocket was closed. - */ - async webSocketClose( - ws: WebSocket, - code: number, - reason: string, - wasClean: boolean, - ) { - const { pubKey, ip, nonce, valid } = ws.deserializeAttachment(); - console.log( - 'webSocketClose', - JSON.stringify({ - pubKey: toB64Url(pubKey), - ip, - nonce: nonce instanceof Uint8Array ? toB64Url(nonce) : nonce, - valid, - code, - reason, - wasClean, - }), - ); - } -} diff --git a/ts/sbd-server/src/msg.test.ts b/ts/sbd-server/src/msg.test.ts index ca521f7..20c5549 100644 --- a/ts/sbd-server/src/msg.test.ts +++ b/ts/sbd-server/src/msg.test.ts @@ -10,7 +10,7 @@ import { MsgNone, MsgForward, } from './msg.ts'; -import { ed } from './ed.ts'; +import * as common from './common.ts'; describe('Msg', () => { it('lbrt', async () => { diff --git a/ts/sbd-server/src/msg.ts b/ts/sbd-server/src/msg.ts index 2beec2b..cb9a6b1 100644 --- a/ts/sbd-server/src/msg.ts +++ b/ts/sbd-server/src/msg.ts @@ -1,6 +1,4 @@ -import { ed } from './ed.ts'; -import { err } from './err.ts'; -import { toB64Url, fromB64Url } from './b64.ts'; +import * as common from './common.ts'; /** * Byte-by-byte comparison of Uint8Array types. @@ -90,13 +88,13 @@ export class Msg { bytes: Uint8Array, ): MsgLbrt | MsgLidl | MsgAreq | MsgAres | MsgSrdy | MsgKeep { if (bytes.byteLength < 32) { - throw err(`invalid msg length ${bytes.byteLength}`, 400); + throw common.err(`invalid msg length ${bytes.byteLength}`, 400); } if (cmp(bytes.subarray(0, 28), CMD)) { if (cmp(bytes.subarray(28, 32), MSG_B_LBRT)) { if (bytes.byteLength !== 32 + 4) { - throw err( + throw common.err( `invalid lbrt msg length, expected 36, got: ${bytes.byteLength}`, 400, ); @@ -105,7 +103,7 @@ export class Msg { return new MsgLbrt(limit); } else if (cmp(bytes.subarray(28, 32), MSG_B_LIDL)) { if (bytes.byteLength !== 32 + 4) { - throw err( + throw common.err( `invalid lidl msg length, expected 36, got: ${bytes.byteLength}`, 400, ); @@ -114,7 +112,7 @@ export class Msg { return new MsgLidl(limit); } else if (cmp(bytes.subarray(28, 32), MSG_B_AREQ)) { if (bytes.byteLength !== 32 + 32) { - throw err( + throw common.err( `invalid areq msg length, expected 64, got: ${bytes.byteLength}`, 400, ); @@ -123,7 +121,7 @@ export class Msg { return new MsgAreq(nonce); } else if (cmp(bytes.subarray(28, 32), MSG_B_ARES)) { if (bytes.byteLength !== 32 + 64) { - throw err( + throw common.err( `invalid ares msg length, expected 96, got: ${bytes.byteLength}`, 400, ); @@ -132,7 +130,7 @@ export class Msg { return new MsgAres(signature); } else if (cmp(bytes.subarray(28, 32), MSG_B_SRDY)) { if (bytes.byteLength !== 32) { - throw err( + throw common.err( `invalid srdy msg length, expected 32, got: ${bytes.byteLength}`, 400, ); @@ -140,7 +138,7 @@ export class Msg { return new MsgSrdy(); } else if (cmp(bytes.subarray(28, 32), MSG_B_KEEP)) { if (bytes.byteLength !== 32) { - throw err( + throw common.err( `invalid keep msg length, expected 32, got: ${bytes.byteLength}`, 400, ); @@ -322,7 +320,7 @@ export class MsgForward extends Msg { */ static build(pubKey: Uint8Array, payload: Uint8Array): MsgForward { if (pubKey.byteLength !== 32) { - throw err( + throw common.err( `invalid pubKey length, expected 32, got: ${pubKey.byteLength}`, 400, ); diff --git a/ts/sbd-server/src/prom.ts b/ts/sbd-server/src/prom.ts new file mode 100644 index 0000000..b3451ba --- /dev/null +++ b/ts/sbd-server/src/prom.ts @@ -0,0 +1,66 @@ +/** + * Prometheus label rendering helper. + */ +function renderLabels(labels: { [index: string]: any }): string { + const out = ['{']; + let isFirst = true; + for (const key in labels) { + if (isFirst) { + isFirst = false; + } else { + out.push(','); + } + out.push(key); + out.push('='); + out.push(JSON.stringify(labels[key])); + } + if (isFirst) { + return ''; + } + out.push('}'); + return out.join(''); +} + +// All the npm modules I could find out there depended on nodejs apis. +// I just need a simple protocol renderer that can be used in cloudflare. + +/** + * Hand-rolled simplistic prometheus metrics renderer. + */ +export class Prom { + #lines: Array; + + constructor() { + this.#lines = []; + } + + /** + * Generate a "guage" item. + */ + guage( + prepend: boolean, + name: string, + help: string, + labels: { [index: string]: any }, + val: number, + ) { + if (prepend) { + this.#lines.unshift(''); + this.#lines.unshift(`${name}${renderLabels(labels)} ${val}`); + this.#lines.unshift(`# TYPE ${name} guage`); + this.#lines.unshift(`# HELP ${name} ${help}`); + } else { + this.#lines.push(`# HELP ${name} ${help}`); + this.#lines.push(`# TYPE ${name} guage`); + this.#lines.push(`${name}${renderLabels(labels)} ${val}`); + this.#lines.push(''); + } + } + + /** + * Render the previous generated prometheus line items into a single string. + */ + render(): string { + return this.#lines.join('\n'); + } +} diff --git a/ts/sbd-server/src/rate-limit.test.ts b/ts/sbd-server/src/rate-limit.test.ts deleted file mode 100644 index 028bc8a..0000000 --- a/ts/sbd-server/src/rate-limit.test.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { describe, expect, assert, it, beforeAll, afterAll } from 'vitest'; -import { RateLimit } from './rate-limit.ts'; - -describe('RateLimit', () => { - it('check multi-node rate limit', async () => { - const addr1 = 'yada1'; - const addr2 = 'yada2'; - - let now = 100; - - const rate = new RateLimit(8000, 16 * 16 * 1024); - - let limitNanosPerByte = null; - - ({ limitNanosPerByte } = rate.bytesReceived(now, addr1, 1)); - - expect(limitNanosPerByte).equals(8000); - - ({ limitNanosPerByte } = rate.bytesReceived(now, addr2, 1)); - - expect(limitNanosPerByte).equals(16000); - - now += 20000; - - ({ limitNanosPerByte } = rate.bytesReceived(now, addr2, 1)); - expect(limitNanosPerByte).equals(8000); - }); - - it('1 to 1 and prune', async () => { - const addr = 'yada'; - - let now = 100n; - let shouldBlock = null; - - const rate = new RateLimit(1, 1); - - // should always be ok when advancing with time - for (let i = 0; i < 10; ++i) { - now += 1n; - - ({ shouldBlock } = rate.bytesReceived(now, addr, 1)); - - assert(!shouldBlock); - } - - // but one more without a time advance fails - ({ shouldBlock } = rate.bytesReceived(now, addr, 1)); - assert(shouldBlock); - - now += 1n; - - // make sure prune doesn't prune it yet - rate.prune(now); - ({ shouldBlock } = rate.bytesReceived(now, addr, 1)); - assert(shouldBlock); - - now += 1n; - - // make sure prune doesn't prune it even after 10 seconds - rate.prune(now + 10000000000n); - ({ shouldBlock } = rate.bytesReceived(now, addr, 1)); - assert(shouldBlock); - - now += 1n; - - // but it *will* after 10 seconds + 1 nanosecond - rate.prune(now + 10000000001n); - ({ shouldBlock } = rate.bytesReceived(now, addr, 1)); - assert(!shouldBlock); - }); - - it('burst', async () => { - const addr = 'yada'; - - let now = 100n; - let shouldBlock = null; - - const rate = new RateLimit(1, 5); - - for (let i = 0; i < 5; ++i) { - ({ shouldBlock } = rate.bytesReceived(now, addr, 1)); - assert(!shouldBlock); - } - - ({ shouldBlock } = rate.bytesReceived(now, addr, 1)); - assert(shouldBlock); - - now += 2n; - - ({ shouldBlock } = rate.bytesReceived(now, addr, 1)); - assert(!shouldBlock); - }); -}); diff --git a/ts/sbd-server/src/rate-limit.ts b/ts/sbd-server/src/rate-limit.ts index 26de16a..7d175ae 100644 --- a/ts/sbd-server/src/rate-limit.ts +++ b/ts/sbd-server/src/rate-limit.ts @@ -1,3 +1,7 @@ +import { DurableObject } from 'cloudflare:workers'; + +import * as common from './common.ts'; + /** * `bytesReceived` call response type. */ @@ -13,52 +17,79 @@ export interface RateLimitResult { shouldBlock: boolean; } -/** - * If a classic number, milliseconds since epoch. - * If a bigint, nanoseconds since epoch. - */ -function nowNanos(now: number | bigint): bigint { - if (typeof now === 'bigint') { - return now; - } else { - const tmp: bigint = BigInt(now); - return tmp * 1000000n; - } +interface State { + map: { [pk: string]: bigint }; + next: bigint; } +enum BlockCheck { + Unchecked, + Unblocked, + Blocked, +} + +const BLOCKED: RateLimitResult = { + limitNanosPerByte: Number.MAX_SAFE_INTEGER, + shouldBlock: true, +}; + /** - * Ratelimit potentially multiple clients coming from the same ip address. + * "RATE_LIMIT" durable object. + * This is a thin wrapper around the "RateLimit" class. */ -export class RateLimit { - map: { [pk: string]: bigint }; - limitNanosPerByte: bigint; - burst: bigint; - - constructor(limitNanosPerByte: number, burst: number) { - this.map = {}; - this.limitNanosPerByte = BigInt(limitNanosPerByte); - this.burst = this.limitNanosPerByte * BigInt(burst); +export class DoRateLimit extends DurableObject { + ctx: DurableObjectState; + env: Env; + lastUnblockCheck: number; + blockCheck: BlockCheck; + state: State; + + constructor(ctx: DurableObjectState, env: Env) { + super(ctx, env); + + this.ctx = ctx; + this.env = env; + + this.lastUnblockCheck = 0; + this.blockCheck = BlockCheck.Unchecked; + + // Note, we're making an explicit decision to NOT store this state + // in either the DO transactional storage or KV store. + // It would cost a lot to keep it updated enough to be useful, + // and we are doing our own memory eviction after 10 seconds anyways. + // Assuming we are not evicted within 10 seconds (docs are vague), + // this will work as well as trying to store it anywhere. + this.state = { map: {}, next: 0n }; } /** - * Clear out any connections older that 10s in the past. - * - * - now: if now is a number, it is milliseconds since epoch - * if now is a bigint, it is nanoseconds since epoch + * Update block state from KV for this IP if it is correct to do so. */ - prune(now: number | bigint) { - const nowNs = nowNanos(now); - - const newMap: { [pk: string]: bigint } = {}; - - for (const pk in this.map) { - const cur = this.map[pk]; - if (nowNs <= cur || nowNs - cur < 10000000000n) { - newMap[pk] = cur; + async checkBlockState(ip: string) { + if (this.blockCheck === BlockCheck.Unblocked) { + // do not check the KV if we are currently unblocked + return; + } else if (this.blockCheck === BlockCheck.Blocked) { + if (Date.now() - this.lastUnblockCheck < 1000 * 60 * 2) { + // if we are blocked, and have checked within the past 2 minutes + // don't bother checking again + return; } + this.lastUnblockCheck = Date.now(); } - this.map = newMap; + // if we made it past the above checks, go ahead and check the kv + const block = await this.env.SBD_COORDINATION.get(`block:${ip}`, { + type: 'json', + }); + + // if we get a `true` back from the kv, mark blocked + // otherwise mark unblocked + if (typeof block === 'boolean' && block) { + this.blockCheck = BlockCheck.Blocked; + } else { + this.blockCheck = BlockCheck.Unblocked; + } } /** @@ -66,41 +97,64 @@ export class RateLimit { * Return a bitrate limit this connection should be following, and * whether that ip has already breached the limit. * - * - now: if now is a number, it is milliseconds since epoch - * if now is a bigint, it is nanoseconds since epoch + * - now: milliseconds since epoch */ - bytesReceived( - now: number | bigint, + async bytesReceived( + now: number, + ip: string, pk: string, bytes: number, - ): RateLimitResult { - this.prune(now); + ): Promise { + return await this.ctx.blockConcurrencyWhile(async () => { + await this.checkBlockState(ip); - const nowNs = nowNanos(now); + if (this.blockCheck === BlockCheck.Blocked) { + return BLOCKED; + } - const rateAdd = BigInt(bytes) * this.limitNanosPerByte; + const nowN = BigInt(now) * 1000000n; - if (!(pk in this.map)) { - this.map[pk] = nowNs; - } + // prune the map - let cur = this.map[pk]; + const newMap: { [pk: string]: bigint } = {}; - if (nowNs > cur) { - cur = nowNs; - } + for (const pk in this.state.map) { + const last = this.state.map[pk]; + // keep if it is newer than 10 seconds + if (last >= nowN - 10000000000n) { + newMap[pk] = last; + } + } - cur += rateAdd; + this.state.map = newMap; - this.map[pk] = cur; + // log the pk last access time - const nextActionInNanos = cur - nowNs; + this.state.map[pk] = nowN; - const shouldBlock = nextActionInNanos > this.burst; + // log the additional bytes - const nodeCount: bigint = BigInt(Object.keys(this.map).length); - const limitNanosPerByte = Number(this.limitNanosPerByte * nodeCount); + if (this.state.next < nowN) { + this.state.next = nowN; + } + + const rateAdd = BigInt(bytes) * BigInt(common.LIMIT_NANOS_PER_BYTE); + + this.state.next += rateAdd; + + const shouldBlock = this.state.next - nowN > common.LIMIT_NANOS_BURST; + const limitNanosPerByte = + common.LIMIT_NANOS_PER_BYTE * Object.keys(this.state.map).length; + + if (shouldBlock) { + this.blockCheck = BlockCheck.Blocked; + await this.env.SBD_COORDINATION.put(`block:${ip}`, 'true', { + expirationTtl: 60 * 10, + }); + return BLOCKED; + } - return { limitNanosPerByte, shouldBlock }; + return { limitNanosPerByte, shouldBlock }; + }); } } diff --git a/ts/sbd-server/src/signal.ts b/ts/sbd-server/src/signal.ts new file mode 100644 index 0000000..0e81553 --- /dev/null +++ b/ts/sbd-server/src/signal.ts @@ -0,0 +1,407 @@ +import { DurableObject } from 'cloudflare:workers'; + +import * as common from './common.ts'; + +import { DoRateLimit } from './rate-limit.ts'; +import { + Msg, + MsgLbrt, + MsgLidl, + MsgAreq, + MsgAres, + MsgSrdy, + MsgKeep, + MsgNone, + MsgForward, +} from './msg.ts'; + +/** + * "SIGNAL" durable object. + */ +export class DoSignal extends DurableObject { + ctx: DurableObjectState; + env: Env; + queue: { [index: string]: Array }; + alarmLock: boolean; + curLimit: number; + active: number; + lastCoord: number; + activeBytesReceived: number; + status: number; + + constructor(ctx: DurableObjectState, env: Env) { + super(ctx, env); + this.ctx = ctx; + this.env = env; + this.queue = {}; + this.alarmLock = false; + this.curLimit = 0; + this.active = common.timestamp(); + this.lastCoord = common.timestamp(); + this.activeBytesReceived = 0; + this.status = 200; + } + + /** + * Client websockets are connected to a durable object identified + * by their own pubKey. When they send forward messages, those must + * be sent to durable objects identified by the destination pubKey. + * This is the api for those messages to be forwarded. + */ + async forward(messageList: Array) { + if (this.status !== 200) { + return; + } + + // MAYBE: buffer messages until handshake complete? + // might not be needed since clients shouldn't publish the address + // until handshake is complete + + // This loop not technically needed, since our fetch ensures there + // is only ever one websocket connected. + for (const ws of this.ctx.getWebSockets()) { + for (const message of messageList) { + try { + ws.send(message); + } catch (e: any) { + console.error('forward error', e); + this.status = 500; + return; + } + } + } + } + + /** + * This is the http endpoint for the "SIGNAL" durable object. + * The worker http fetch endpoint above forwards the request here. + * This function performs some checks, then upgrades the connection + * to a websocket. + */ + async fetch(request: Request): Promise { + if (this.status !== 200) { + return new Response(JSON.stringify({ err: 'dead' }), { + status: this.status, + }); + } + + let cleanServer = null; + try { + const ip = request.headers.get('cf-connecting-ip') || 'no-ip'; + const url = new URL(request.url); + + const { pubKeyStr, pubKeyBytes } = common.parsePubKey(url.pathname); + + if (this.ctx.getWebSockets().length > 0) { + throw common.err('websocket already connected', 400); + } + if (request.headers.get('Upgrade') !== 'websocket') { + throw common.err('expected websocket', 426); + } + + const [client, server] = Object.values(new WebSocketPair()); + + this.ctx.acceptWebSocket(server); + cleanServer = server; + + const nonce = new Uint8Array(32); + crypto.getRandomValues(nonce); + + const opened = common.timestamp(); + + server.serializeAttachment({ + pubKey: pubKeyBytes, + ip, + nonce, + valid: false, + opened, + }); + + // this will also send MsgLbrt + await this.ipRateLimit(ip, pubKeyStr, 1, server); + + server.send(new MsgLidl(common.LIMIT_IDLE_MILLIS).encoded()); + server.send(new MsgAreq(nonce).encoded()); + + console.log( + 'webSocketOpen', + JSON.stringify({ + opened, + active: this.active, + pubKey: pubKeyStr, + ip, + nonce: common.toB64Url(nonce), + }), + ); + + return new Response(null, { status: 101, webSocket: client }); + } catch (e: any) { + // Note: one might be tempted to set this.status here, but then bad + // actors could maliciously drop other peer connections just + // by trying (and failing) to connect to them. + + console.error('error', e.toString()); + if (cleanServer) { + // Note: HERE it's okay to set this.status, since we know we're + // the original connecting websocket. + this.status = e.status || 500; + cleanServer.close(4000 + (e.status || 500), e.toString()); + } + return new Response(JSON.stringify({ err: e.toString() }), { + status: e.status || 500, + }); + } + } + + /** + * Helper function for performing the rate-limit check and + * closing the websocket if we violate the limit. + */ + async ipRateLimit(ip: string, pk: string, bytes: number, ws: WebSocket) { + try { + const ipId = this.env.RATE_LIMIT.idFromName(ip); + const ipStub = this.env.RATE_LIMIT.get( + ipId, + ) as DurableObjectStub; + const { limitNanosPerByte, shouldBlock } = await ipStub.bytesReceived( + Date.now(), + ip, + pk, + bytes, + ); + if (shouldBlock) { + throw common.err(`limit`, 429); + } + if (this.curLimit !== limitNanosPerByte) { + this.curLimit = limitNanosPerByte; + ws.send(new MsgLbrt(limitNanosPerByte).encoded()); + } + } catch (e) { + throw common.err(`limit ${e}`, 429); + } + } + + /** + * Handle incoming websocket messages. + * First handshake the connection, then start handling forwarding messages. + */ + async webSocketMessage(ws: WebSocket, message: ArrayBuffer | string) { + if (this.status !== 200) { + ws.close(4000 + this.status, 'dead'); + return; + } + + await this.ctx.blockConcurrencyWhile(async () => { + try { + const { pubKey, ip, nonce, valid, opened } = ws.deserializeAttachment(); + if (!pubKey) { + throw common.err('no associated pubKey'); + } + if (!ip) { + throw common.err('no associated ip'); + } + + // convert strings into binary + let msgRaw: Uint8Array; + if (message instanceof ArrayBuffer) { + msgRaw = new Uint8Array(message); + } else { + const enc = new TextEncoder(); + msgRaw = enc.encode(message); + } + + this.activeBytesReceived += msgRaw.byteLength; + await this.ipRateLimit(ip, pubKey, msgRaw.byteLength, ws); + + if (msgRaw.byteLength > common.MAX_MESSAGE_BYTES) { + throw common.err('max message length exceeded', 400); + } + + const pubKeyStr = common.toB64Url(pubKey); + + const msg = Msg.parse(msgRaw); + + if (!valid) { + if (!nonce) { + throw common.err('no associated nonce'); + } + + if (msg instanceof MsgAres) { + if (!common.ed.verify(msg.signature(), nonce, pubKey)) { + throw common.err('invalid handshake signature', 400); + } + + ws.send(new MsgSrdy().encoded()); + + ws.serializeAttachment({ + pubKey, + ip, + nonce: true, // don't need to keep the actual nonce anymore + valid: true, + opened, + }); + + console.log( + 'webSocketAuthenticated', + JSON.stringify({ + opened, + active: this.active, + pubKey: pubKeyStr, + }), + ); + + this.lastCoord = common.timestamp(); + const metadata = { + op: opened, + ac: this.active, + br: this.activeBytesReceived, + ip, + }; + await this.env.SBD_COORDINATION.put( + `client:${pubKeyStr}`, + JSON.stringify(metadata), + { expirationTtl: 60, metadata }, + ); + } else { + if (msg instanceof MsgForward) { + throw common.err(`invalid forward before handshake`); + } + // otherwise just ignore the message + } + } else { + if (common.timestamp() - this.lastCoord >= 30) { + this.lastCoord = common.timestamp(); + const metadata = { + op: opened, + ac: this.active, + br: this.activeBytesReceived, + ip, + }; + await this.env.SBD_COORDINATION.put( + `client:${pubKeyStr}`, + JSON.stringify(metadata), + { expirationTtl: 60, metadata }, + ); + } + + if (msg instanceof MsgNone) { + // no-op + } else if (msg instanceof MsgKeep) { + // keep alive + } else if (msg instanceof MsgForward) { + // extract the destination pubKey (slice does a copy) + const dest = msg.pubKey().slice(0); + + // overwrite the destination pubKey with the source (our) pubKey + // the pubKey() function returns a reference, so editing it + // alters the message that will be sent + msg.pubKey().set(pubKey, 0); + + const id = common.toB64Url(dest); + if (!this.queue[id]) { + this.queue[id] = []; + } + this.queue[id].push(msg.encoded()); + + if (!this.alarmLock) { + const alarm = await this.ctx.storage.getAlarm(); + if (!alarm) { + this.ctx.storage.setAlarm(Date.now() + common.BATCH_DUR_MS); + } + } + } else { + throw common.err( + `invalid post-handshake message type: ${msg.type()}`, + ); + } + } + } catch (e: any) { + console.error('error', e.toString()); + this.status = e.status || 500; + ws.close(4000 + this.status, e.toString()); + } + }); + } + + /** + * The `webSocketMessage` handler above enqueues messages for delivery, + * then sets up an alarm to handle actually forwarding them. This ensures + * the messages are delivered in order without deadlocking two clients + * that happened to try to forward messages to each other at the same moment. + */ + async alarm() { + if (this.status !== 200) { + return; + } + + const { shouldReturn, queue } = await this.ctx.blockConcurrencyWhile( + async () => { + if (this.alarmLock || Object.keys(this.queue).length === 0) { + return { shouldReturn: true, queue: {} }; + } + this.alarmLock = true; + const queue = this.queue; + this.queue = {}; + return { shouldReturn: false, queue }; + }, + ); + + if (shouldReturn) { + return; + } + + // We cannot do the actual forwarding within a blockConcurrency because + // then if two peers try to send each other data at the same time it + // will deadlock. Hence all the complexity with the alarms and alarmLock. + + for (const idName in queue) { + try { + const id = this.env.SIGNAL.idFromName(idName); + const stub = this.env.SIGNAL.get(id) as DurableObjectStub; + + await stub.forward(queue[idName]); + } catch (_e: any) { + // It is okay to get errors forwarding to peers, they may have + // disconnected. We still want to forward to other peers who + // may still be there. + } + } + + await this.ctx.blockConcurrencyWhile(async () => { + this.alarmLock = false; + + if (Object.keys(this.queue).length !== 0) { + const alarm = await this.ctx.storage.getAlarm(); + if (!alarm) { + this.ctx.storage.setAlarm(Date.now() + common.BATCH_DUR_MS); + } + } + }); + } + + /** + * The websocket was closed. + */ + async webSocketClose( + ws: WebSocket, + code: number, + reason: string, + wasClean: boolean, + ) { + const { pubKey, ip, nonce, valid, opened } = ws.deserializeAttachment(); + console.log( + 'webSocketClose', + JSON.stringify({ + opened, + active: this.active, + pubKey: common.toB64Url(pubKey), + ip, + nonce: nonce instanceof Uint8Array ? common.toB64Url(nonce) : nonce, + valid, + code, + reason, + wasClean, + }), + ); + } +} diff --git a/ts/sbd-server/src/tsconfig.json b/ts/sbd-server/src/tsconfig.json new file mode 100644 index 0000000..5197ce2 --- /dev/null +++ b/ts/sbd-server/src/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../tsconfig.json", + "include": ["./**/*.ts"] +} diff --git a/ts/sbd-server/test/rate-limit.test.ts b/ts/sbd-server/test/rate-limit.test.ts new file mode 100644 index 0000000..14f51a2 --- /dev/null +++ b/ts/sbd-server/test/rate-limit.test.ts @@ -0,0 +1,93 @@ +import { env, runInDurableObject } from 'cloudflare:test'; +import { describe, it, expect } from 'vitest'; +import { DoRateLimit } from '../src'; + +it('rate-limit sanity', async () => { + const test = '1'; + const ip = `ip${test}`; + const pk = `pk${test}`; + + const id = env.RATE_LIMIT.idFromName(ip); + const stub = env.RATE_LIMIT.get(id); + await runInDurableObject(stub, async (inst: DoRateLimit) => { + const res = await inst.bytesReceived(Date.now(), ip, pk, 1); + expect(res.shouldBlock).equals(false); + }); +}); + +it('rate-limit causes block', async () => { + const test = '2'; + const ip = `ip${test}`; + const pk = `pk${test}`; + + const id = env.RATE_LIMIT.idFromName(ip); + const stub = env.RATE_LIMIT.get(id); + await runInDurableObject(stub, async (inst: DoRateLimit) => { + const res = await inst.bytesReceived( + Date.now(), + ip, + pk, + Number.MAX_SAFE_INTEGER, + ); + expect(res.shouldBlock).equals(true); + }); + + const kvBlock = await env.SBD_COORDINATION.get(`block:${ip}`, { + type: 'json', + }); + expect(kvBlock).equals(true); +}); + +it('rate-limit load checks kv block', async () => { + const test = '3'; + const ip = `ip${test}`; + const pk = `pk${test}`; + + await env.SBD_COORDINATION.put(`block:${ip}`, 'true', { + expirationTtl: 60 * 10, + }); + + const id = env.RATE_LIMIT.idFromName(ip); + const stub = env.RATE_LIMIT.get(id); + await runInDurableObject(stub, async (inst: DoRateLimit) => { + const res = await inst.bytesReceived(Date.now(), ip, pk, 1); + expect(res.shouldBlock).equals(true); + }); +}); + +it('rate-limit slower for multi-con ips', async () => { + const test = '4'; + const ip = `ip${test}`; + + const id = env.RATE_LIMIT.idFromName(ip); + const stub = env.RATE_LIMIT.get(id); + + const rate1 = await runInDurableObject(stub, async (inst: DoRateLimit) => { + const res = await inst.bytesReceived(Date.now(), ip, 'pkA', 1); + return res.limitNanosPerByte; + }); + + const rate2 = await runInDurableObject(stub, async (inst: DoRateLimit) => { + const res = await inst.bytesReceived(Date.now(), ip, 'pkB', 1); + return res.limitNanosPerByte; + }); + + expect(rate2).equals(rate1 * 2); + + const rate3 = await runInDurableObject(stub, async (inst: DoRateLimit) => { + const res = await inst.bytesReceived(Date.now(), ip, 'pkC', 1); + return res.limitNanosPerByte; + }); + + expect(rate3).equals(rate1 * 3); + + const rate1Again = await runInDurableObject( + stub, + async (inst: DoRateLimit) => { + const res = await inst.bytesReceived(Date.now(), ip, 'pkA', 1); + return res.limitNanosPerByte; + }, + ); + + expect(rate1Again).equals(rate3); +}); diff --git a/ts/sbd-server/test/tsconfig.json b/ts/sbd-server/test/tsconfig.json new file mode 100644 index 0000000..21fc1b6 --- /dev/null +++ b/ts/sbd-server/test/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "moduleResolution": "bundler", + "types": [ + "@cloudflare/workers-types/experimental", + "@cloudflare/vitest-pool-workers" + ] + }, + "include": ["./**/*.ts", "../src/env.d.ts"] +} diff --git a/ts/sbd-server/tsconfig.json b/ts/sbd-server/tsconfig.json index 38f988f..9edf8ed 100644 --- a/ts/sbd-server/tsconfig.json +++ b/ts/sbd-server/tsconfig.json @@ -5,7 +5,8 @@ "module": "es2022", "moduleResolution": "node", "types": [ - "@cloudflare/workers-types/2023-07-01" + "@cloudflare/workers-types/2023-07-01", + "@cloudflare/vitest-pool-workers" ], "resolveJsonModule": true, "allowImportingTsExtensions": true, @@ -17,5 +18,6 @@ "forceConsistentCasingInFileNames": true, "strict": true, "skipLibCheck": true - } + }, + "include": ["./*.ts"] } diff --git a/ts/sbd-server/vitest.config.js b/ts/sbd-server/vitest.config.js new file mode 100644 index 0000000..973627c --- /dev/null +++ b/ts/sbd-server/vitest.config.js @@ -0,0 +1,11 @@ +import { defineWorkersConfig } from "@cloudflare/vitest-pool-workers/config"; + +export default defineWorkersConfig({ + test: { + poolOptions: { + workers: { + wrangler: { configPath: "./wrangler.toml" }, + }, + }, + }, +}); diff --git a/ts/sbd-server/wrangler.toml b/ts/sbd-server/wrangler.toml index 5f083a3..d645952 100644 --- a/ts/sbd-server/wrangler.toml +++ b/ts/sbd-server/wrangler.toml @@ -1,10 +1,19 @@ name = "sbd" main = "src/index.ts" compatibility_date = "2024-06-03" +compatibility_flags = [ "nodejs_compat" ] route = "sbd.holo.host/*" account_id = "18ff2b4e6205b938652998cfca0d8cff" +minify = true workers_dev = false +#[limits] +#cpu_ms = 5000 + +kv_namespaces = [ + { binding = "SBD_COORDINATION", id = "cde857dd4ea745fea55f80daa52a6d5c" } +] + [[durable_objects.bindings]] name = "SIGNAL" class_name = "DoSignal"