From 580a943c77291a8d49192d8ddb6f90089195eabb Mon Sep 17 00:00:00 2001 From: Gabi Date: Mon, 23 Sep 2024 21:55:32 +0200 Subject: [PATCH] registry: garbage collector (experimental) (#48) * registry: add garbage collection implementation by @IvanDev * tests: transition to new vitest runner * registry: add concurrency safety in the garbage collector * test: move tests to a folder * registry: add more docs around garbage collection and improve the 'locking' --------- Co-authored-by: Ivan Isaev <> --- .github/workflows/test.yml | 4 +- docs/garbage-collection.md | 119 +++ index.ts | 2 +- package.json | 20 +- pnpm-lock.yaml | 1076 +++++++-------------------- src/chunk.ts | 1 - src/errors.ts | 24 + src/manifest.ts | 31 + src/registry/garbage-collector.ts | 233 ++++++ src/registry/http.ts | 5 + src/registry/r2.ts | 88 ++- src/registry/registry.ts | 3 + src/router.ts | 10 + index.test.ts => test/index.test.ts | 180 +++-- test/tsconfig.json | 13 + test/vitest.config.ts | 12 + test/wrangler.test.toml | 21 + tsconfig.base.json | 2 +- tsconfig.json | 4 +- vitest.config.ts | 13 - wrangler.toml.example | 4 +- 21 files changed, 943 insertions(+), 922 deletions(-) create mode 100644 docs/garbage-collection.md create mode 100644 src/manifest.ts create mode 100644 src/registry/garbage-collector.ts rename index.test.ts => test/index.test.ts (68%) create mode 100644 test/tsconfig.json create mode 100644 test/vitest.config.ts create mode 100644 test/wrangler.test.toml delete mode 100644 vitest.config.ts diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 31d3fbc..20cb85f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -17,12 +17,12 @@ jobs: - name: Install pnpm uses: pnpm/action-setup@v4 with: - version: 9 + version: 9.9 - name: Use Node uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} - cache: 'pnpm' + cache: "pnpm" - run: pnpm install --frozen-lockfile --child-concurrency=10 - run: cp wrangler.toml.example wrangler.toml diff --git a/docs/garbage-collection.md b/docs/garbage-collection.md new file mode 100644 index 0000000..59bccd2 --- /dev/null +++ b/docs/garbage-collection.md @@ -0,0 +1,119 @@ +# Removing manifests and garbage collection + +Garbage collection is useful due to how [OCI](https://github.com/opencontainers/image-spec/blob/main/manifest.md) container images get shipped to registries. + +```json +{ + "schemaVersion": 2, + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "config": { + "mediaType": "application/vnd.oci.image.config.v1+json", + "digest": "sha256:b5b2b2c507a0944348e0303114d8d93aaaa081732b86451d9bce1f432a537bc7", + "size": 7023 + }, + "layers": [ + { + "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", + "digest": "sha256:9834876dcfb05cb167a5c24953eba58c4ac89b1adf57f28f2f9d09af107ee8f0", + "size": 32654 + }, + { + "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", + "digest": "sha256:3c3a4604a545cdc127456d94e421cd355bca5b528f4a9c1905b15da2eb4a4c6b", + "size": 16724 + }, + { + "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", + "digest": "sha256:ec4b8955958665577945c89419d1af06b5f7636b4ac3da7f12184802ad867736", + "size": 73109 + } + ] +} +``` + +This is how a container image manifest looks, it's a "tree-like" structure where the manifest references +layers. If you remove an image from a registry, you're probably just removing its manifest. However, layers +will still be around taking space. + +## Removing an image and triggering the garbage collection + +To delete an image of your registry, you can use `skopeo delete` or an API call: + +``` +# If you pushed to serverless.workers.dev/my-image:latest +curl -X DELETE -X "Authorization: $CREDENTIAL" https://serverless.workers.dev/my-image/manifests/latest +# You will also need to remove the digest reference +curl -X DELETE -X "Authorization: $CREDENTIAL" https://serverless.workers.dev/my-image/manifests/ +``` + +The layer still exists in the registry, but we can remove it by triggering the garbage collector. + +``` +curl -X POST -H "Authorization: $CREDENTIAL" https://serverless.workers.dev/my-image/gc +{"success":true} +``` + +## How does it work + +How do we remove them? We take the approach of listing all manifests in a namespace and storing its digests +in a Set, then we list all the layers and those that are not in the Set get removed. That has a big drawback +that means we might be removing layers that don't have a manifest but are about to have one at the end of their push. + +In serverless-registry, if we remove a layer garbage collecting the manifest endpoint will throw a BLOB_UNKNOWN +error, but the garbage collector can still race with that endpont, so we go back to square one. + +Some registries take a lock stop the world approach, however serverless-registry can't really do that due +to its objective of only using R2. However, we need to fail whenever a race condition happens, a data +race that causes data-loss would be completely unacceptable. + +That's when we introduce a simple system where instead of taking a lock, we mark in R2 +that we are about to create a manifest and that we are inserting data. +If the garbage collector starts and sees that key, it will fail. At the end of the insertion, the insertion mark +gets updated. + +The same goes for the garbage collector, when it starts it creates a mark, and when it finishes it updates the +mark. + +Let's state some scenarios: + +``` +PutManifest GC +1. markForInsertion() 2. markForGarbageCollection() +... +3. checkLayersExist() ... +4. checkGCDidntStart() // fails due to ongoing gc +5. insertManifest() +``` + +``` +PutManifest GC +1. markForInsertion() 4. markForGarbageCollection() +... +2. checkLayersExist() 6. mark = getInsertionMark(); +3. checkGCDidntStart() 7. ... finds a layer to remove +5. insertManifest() 8. checkOnGoingUpdates() // fails due to ongoing updates +9. unmarkForInsertion() +``` + +``` +PutManifest GC +1. markForInsertion() 4. markForGarbageCollection() +... +2. checkLayersExist() 6. mark = getInsertionMark(); +3. checkGCDidntStart() 7. ... finds a layer to remove +5. insertManifest() 9. checkOnGoingUpdates() +8. unmarkForInsertion() 10. checkMark(mark) // this fails, not latest, can't delete layer +``` + +``` +PutManifest GC +4. markForInsertion() 1. markForGarbageCollection() +5. gcMark = getGCMark() +6. checkLayersExist() 2. mark = getInsertionMark(); + 3. checkOngoingUpdates() and checkMark(mark) + 7. deleteLayer() and unmarkGarbageCollector(); +8. checkGCDidntStart(gcMark) // fails because latest gc marker is different +``` + +It's a pattern where you build the state you need a lock in, get the mark of when you built that world, +and confirm before making changes from that view that there is nothing that might've changed the view. diff --git a/index.ts b/index.ts index 585c600..05c5a19 100644 --- a/index.ts +++ b/index.ts @@ -77,7 +77,7 @@ export default { return new InternalError(); } }, -}; +} satisfies ExportedHandler; const ensureConfig = (env: Env): boolean => { if (!env.REGISTRY) { diff --git a/package.json b/package.json index 68a95ed..311069d 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "deploy": "wrangler publish", "dev:miniflare": "cross-env NODE_ENV=development wrangler --env dev dev --port 9999 --live-reload", "typecheck": "tsc", - "test": "cross-env NODE_OPTIONS=--experimental-vm-modules vitest run" + "test": "vitest --config test/vitest.config.ts run" }, "dependencies": { "@cfworker/base64url": "^1.12.5", @@ -16,23 +16,23 @@ "zod": "^3.22.4" }, "devDependencies": { + "@cloudflare/vitest-pool-workers": "^0.5.7", "@cloudflare/workers-types": "^4.20240614.0", "cross-env": "^7.0.3", "eslint": "^8.57.0", - "miniflare": "3.20240208.0", + "miniflare": "3.20240909.4", "typescript": "^5.3.3", - "vitest": "^1.3.1", - "vitest-environment-miniflare": "^2.14.2", - "wrangler": "^3.61.0" + "vitest": "^2.1.0", + "wrangler": "^3.78.7" }, "engines": { "node": ">=18" }, "author": "", "license": "Apache-2.0", - "pnpm": { - "overrides": { - "@types/node": "18.15.3" - } - } + "pnpm": { + "overrides": { + "@types/node": "18.15.3" + } + } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7157c89..e98e31c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -24,6 +24,9 @@ importers: specifier: ^3.22.4 version: 3.22.4 devDependencies: + '@cloudflare/vitest-pool-workers': + specifier: ^0.5.7 + version: 0.5.7(@cloudflare/workers-types@4.20240614.0)(@vitest/runner@2.1.1)(@vitest/snapshot@2.1.1)(vitest@2.1.1(@types/node@18.15.3)) '@cloudflare/workers-types': specifier: ^4.20240614.0 version: 4.20240614.0 @@ -34,20 +37,17 @@ importers: specifier: ^8.57.0 version: 8.57.0 miniflare: - specifier: 3.20240208.0 - version: 3.20240208.0 + specifier: 3.20240909.4 + version: 3.20240909.4 typescript: specifier: ^5.3.3 version: 5.3.3 vitest: - specifier: ^1.3.1 - version: 1.3.1(@types/node@18.15.3) - vitest-environment-miniflare: - specifier: ^2.14.2 - version: 2.14.2(vitest@1.3.1(@types/node@18.15.3)) + specifier: ^2.1.0 + version: 2.1.1(@types/node@18.15.3) wrangler: - specifier: ^3.61.0 - version: 3.61.0(@cloudflare/workers-types@4.20240614.0) + specifier: ^3.78.7 + version: 3.78.7(@cloudflare/workers-types@4.20240614.0) packages: @@ -58,69 +58,50 @@ packages: '@cfworker/base64url@1.12.5': resolution: {integrity: sha512-pNLrz0D0MguzFLJisBUv+XOTkpRpRTIMI7/r2QwTWI2MR5VJ7Hysd6ug6DBWksKFy7TK3hCf+qejufdJSN5X+A==} - '@cloudflare/kv-asset-handler@0.3.3': - resolution: {integrity: sha512-wpE+WiWW2kUNwNE0xyl4CtTAs+STjGtouHGiZPGRaisGB7eXXdbvfZdOrQJQVKgTxZiNAgVgmc7fj0sUmd8zyA==} + '@cloudflare/kv-asset-handler@0.3.4': + resolution: {integrity: sha512-YLPHc8yASwjNkmcDMQMY35yiWjoKAKnhUbPRszBRS0YgH+IXtsMp61j+yTcnCE3oO2DgP0U3iejLC8FTtKDC8Q==} engines: {node: '>=16.13'} - '@cloudflare/workerd-darwin-64@1.20240208.0': - resolution: {integrity: sha512-64qjsCUz6VtjXnUex5D6dWoJDuUBRw1ps2TEVH9wGJ4ubiLVUxKhj3bzkVy0RoJ8FhaCKzJWWRyTo4yc192UTA==} - engines: {node: '>=16'} - cpu: [x64] - os: [darwin] + '@cloudflare/vitest-pool-workers@0.5.7': + resolution: {integrity: sha512-CNMmp0nk0IpVGX/B2mYA2os70K9qkYDnEbj80+r9mrsKO+fxMUoKqoGIDUxiajRVJLxAPwUW4ZsKSGpY65I12A==} + peerDependencies: + '@vitest/runner': 2.0.x - 2.1.x + '@vitest/snapshot': 2.0.x - 2.1.x + vitest: 2.0.x - 2.1.x - '@cloudflare/workerd-darwin-64@1.20240610.1': - resolution: {integrity: sha512-YanZ1iXgMGaUWlleB5cswSE6qbzyjQ8O7ENWZcPAcZZ6BfuL7q3CWi0t9iM1cv2qx92rRztsRTyjcfq099++XQ==} + '@cloudflare/workerd-darwin-64@1.20240909.0': + resolution: {integrity: sha512-nJ8jm/6PR8DPzVb4QifNAfSdrFZXNblwIdOhLTU5FpSvFFocmzFX5WgzQagvtmcC9/ZAQyxuf7WynDNyBcoe0Q==} engines: {node: '>=16'} cpu: [x64] os: [darwin] - '@cloudflare/workerd-darwin-arm64@1.20240208.0': - resolution: {integrity: sha512-eVQrAV200LhwLY6JZLx3l2lDrjsTC86lqnvH+RSeM43bAcdneC6lVfykHnTaOTgYFvYQbqRkn9ICWxXj1V9L5g==} + '@cloudflare/workerd-darwin-arm64@1.20240909.0': + resolution: {integrity: sha512-gJqKa811oSsoxy9xuoQn7bS0Hr1sY+o3EUORTcEnulG6Kz9NQ6nd8QNdp2Hrk2jmmSqwrNkn+a6PZkWzk6Q0Gw==} engines: {node: '>=16'} cpu: [arm64] os: [darwin] - '@cloudflare/workerd-darwin-arm64@1.20240610.1': - resolution: {integrity: sha512-bRe/y/LKjIgp3L2EHjc+CvoCzfHhf4aFTtOBkv2zW+VToNJ4KlXridndf7LvR9urfsFRRo9r4TXCssuKaU+ypQ==} - engines: {node: '>=16'} - cpu: [arm64] - os: [darwin] - - '@cloudflare/workerd-linux-64@1.20240208.0': - resolution: {integrity: sha512-ivZ2UuCvi44j8JZ++XlQzSYajt5ptvAdwlh3WPpCcygtHXEh6SVo8QXEUOXhPbv861C0HZMYxLCaLqlpQDWB8g==} - engines: {node: '>=16'} - cpu: [x64] - os: [linux] - - '@cloudflare/workerd-linux-64@1.20240610.1': - resolution: {integrity: sha512-2zDcadR7+Gs9SjcMXmwsMji2Xs+yASGNA2cEHDuFc4NMUup+eL1mkzxc/QzvFjyBck98e92rBjMZt2dVscpGKg==} + '@cloudflare/workerd-linux-64@1.20240909.0': + resolution: {integrity: sha512-sJrmtccfMg73sZljiBpe4R+lhF58TqzqhF2pQG8HRjyxkzkM1sjpZqfEFaIkNUDqd3/Ibji49fklhPCGXljKSg==} engines: {node: '>=16'} cpu: [x64] os: [linux] - '@cloudflare/workerd-linux-arm64@1.20240208.0': - resolution: {integrity: sha512-aLfvl9kXQKbM7aLvfL0HbOt5VEgv15mEZGyFKyDldJ8+nOXH6nYPma1ccwF8BHmu8otHc420eyPr2xPKhLSJnw==} - engines: {node: '>=16'} - cpu: [arm64] - os: [linux] - - '@cloudflare/workerd-linux-arm64@1.20240610.1': - resolution: {integrity: sha512-7y41rPi5xmIYJN8CY+t3RHnjLL0xx/WYmaTd/j552k1qSr02eTE2o/TGyWZmGUC+lWnwdPQJla0mXbvdqgRdQg==} + '@cloudflare/workerd-linux-arm64@1.20240909.0': + resolution: {integrity: sha512-dTbSdceyRXPOSER+18AwYRbPQG0e/Dwl2trmfMMCETkfJhNLv1fU3FFMJPjfILijKnhTZHSnHCx0+xwHdon2fg==} engines: {node: '>=16'} cpu: [arm64] os: [linux] - '@cloudflare/workerd-windows-64@1.20240208.0': - resolution: {integrity: sha512-Y6KMukWnorsSmPx6d82IuJ4SU8sX1+2y+w1uFJ76sucSgXqUAN1fmjG+EyzRVbcbsxRGBCD9c1Pn8T1amMLEYA==} + '@cloudflare/workerd-windows-64@1.20240909.0': + resolution: {integrity: sha512-/d4BT0kcWFa7Qc0K4K9+cwVQ1qyPNKiO42JZUijlDlco+TYTPkLO3qGEohmwbfMq+BieK7JTMSgjO81ZHpA0HQ==} engines: {node: '>=16'} cpu: [x64] os: [win32] - '@cloudflare/workerd-windows-64@1.20240610.1': - resolution: {integrity: sha512-B0LyT3DB6rXHWNptnntYHPaoJIy0rXnGfeDBM3nEVV8JIsQrx8MEFn2F2jYioH1FkUVavsaqKO/zUosY3tZXVA==} - engines: {node: '>=16'} - cpu: [x64] - os: [win32] + '@cloudflare/workers-shared@0.5.3': + resolution: {integrity: sha512-Yk5Im7zsyKbzd7qi+DrL7ZJR9+bdZwq9BqZWS4muDIWA8MCUeSLsUC+C9u+jdwfPSi5It2AcQG4f0iwZr6jkkQ==} + engines: {node: '>=16.7.0'} '@cloudflare/workers-types@4.20240614.0': resolution: {integrity: sha512-fnV3uXD1Hpq5EWnY7XYb+smPcjzIoUFiZpTSV/Tk8qKL3H+w6IqcngZwXQBZ/2U/DwYkDilXHW3FfPhnyD7FZA==} @@ -442,13 +423,6 @@ packages: '@humanwhocodes/object-schema@2.0.2': resolution: {integrity: sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==} - '@iarna/toml@2.2.5': - resolution: {integrity: sha512-trnsAYxU3xnS1gPHPyU961coFyLkh4gAD/0zQ5mymY4yOZ+CYvsPqUbOFSw0aDM4y0tV7tiFxL/1XfXPNC6IPg==} - - '@jest/schemas@29.6.3': - resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - '@jridgewell/resolve-uri@3.1.2': resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} engines: {node: '>=6.0.0'} @@ -456,73 +430,12 @@ packages: '@jridgewell/sourcemap-codec@1.4.15': resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} + '@jridgewell/sourcemap-codec@1.5.0': + resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} + '@jridgewell/trace-mapping@0.3.9': resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} - '@miniflare/cache@2.14.2': - resolution: {integrity: sha512-XH218Y2jxSOfxG8EyuprBKhI/Fn6xLrb9A39niJBlzpiKXqr8skl/sy/sUL5tfvqEbEnqDagGne8zEcjM+1fBg==} - engines: {node: '>=16.13'} - - '@miniflare/core@2.14.2': - resolution: {integrity: sha512-n/smm5ZTg7ilGM4fxO7Gxhbe573oc8Za06M3b2fO+lPWqF6NJcEKdCC+sJntVFbn3Cbbd2G1ChISmugPfmlCkQ==} - engines: {node: '>=16.13'} - - '@miniflare/d1@2.14.2': - resolution: {integrity: sha512-3NPJyBLbFfzz9VAAdIZrDRdRpyslVCJoZHQk0/0CX3z2mJIfcQzjZhox2cYCFNH8NMJ7pRg6AeSMPYAnDKECDg==} - engines: {node: '>=16.7'} - - '@miniflare/durable-objects@2.14.2': - resolution: {integrity: sha512-BfK+ZkJABoi7gd/O6WbpsO4GrgW+0dmOBWJDlNBxQ7GIpa+w3n9+SNnrYUxKzWlPSvz+TfTTk381B1z/Z87lPw==} - engines: {node: '>=16.13'} - - '@miniflare/html-rewriter@2.14.2': - resolution: {integrity: sha512-tu0kd9bj38uZ04loHb3sMI8kzUzZPgPOAJEdS9zmdSPh0uOkjCDf/TEkKsDdv2OFysyb0DRsIrwhPqCTIrPf1Q==} - engines: {node: '>=16.13'} - - '@miniflare/kv@2.14.2': - resolution: {integrity: sha512-3rs4cJOGACT/U7NH7j4KD29ugXRYUiM0aGkvOEdFQtChXLsYClJljXpezTfJJxBwZjdS4F2UFTixtFcHp74UfA==} - engines: {node: '>=16.13'} - - '@miniflare/queues@2.14.2': - resolution: {integrity: sha512-OylkRs4lOWKvGnX+Azab3nx+1qwC87M36/hkgAU1RRvVDCOxOrYLvNLUczFfgmgMBwpYsmmW8YOIASlI3p4Qgw==} - engines: {node: '>=16.7'} - - '@miniflare/r2@2.14.2': - resolution: {integrity: sha512-uuc7dx6OqSQT8i0F2rsigfizXmInssRvvJAjoi1ltaNZNJCHH9l1PwHfaNc/XAuDjYmiCjtHDaPdRvZU9g9F3g==} - engines: {node: '>=16.13'} - - '@miniflare/runner-vm@2.14.2': - resolution: {integrity: sha512-WlyxAQ+bv/9Pm/xnbpgAg7RNX4pz/q3flytUoo4z4OrRmNEuXrbMUsJZnH8dziqzrZ2gCLkYIEzeaTmSQKp5Jg==} - engines: {node: '>=16.13'} - - '@miniflare/shared-test-environment@2.14.2': - resolution: {integrity: sha512-97y/95EzDC86jM2bKYacKk4n59miFC+WXueRdW5TeVzBw2UTL2Alvy36AZuyMUgBqxZdJSv88/q0jHTw7LvyMA==} - engines: {node: '>=16.13'} - - '@miniflare/shared@2.14.2': - resolution: {integrity: sha512-dDnYIztz10zDQjaFJ8Gy9UaaBWZkw3NyhFdpX6tAeyPA/2lGvkftc42MYmNi8s5ljqkZAtKgWAJnSf2K75NCJw==} - engines: {node: '>=16.13'} - - '@miniflare/sites@2.14.2': - resolution: {integrity: sha512-jFOx1G5kD+kTubsga6jcFbMdU2nSuNG2/EkojwuhYT8hYp3qd8duvPyh1V+OR2tMvM4FWu6jXPXNZNBHXHQaUQ==} - engines: {node: '>=16.13'} - - '@miniflare/storage-file@2.14.2': - resolution: {integrity: sha512-tn8rqMBeTtN+ICHQAMKQ0quHGYIkcyDK0qKW+Ic14gdfGDZx45BqXExQM9wTVqKtwAt85zp5eKVUYQCFfUx46Q==} - engines: {node: '>=16.13'} - - '@miniflare/storage-memory@2.14.2': - resolution: {integrity: sha512-9Wtz9mayHIY0LDsfpMGx+/sfKCq3eAQJzYY+ju1tTEaKR0sVAuO51wu0wbyldsjj9OcBcd2X32iPbIa7KcSZQQ==} - engines: {node: '>=16.13'} - - '@miniflare/watcher@2.14.2': - resolution: {integrity: sha512-/TL0np4uYDl+6MdseDApZmDdlJ6Y7AY5iDY0TvUQJG9nyBoCjX6w0Zn4SiKDwO6660rPtSqZ5c7HzbPhGb5vsA==} - engines: {node: '>=16.13'} - - '@miniflare/web-sockets@2.14.2': - resolution: {integrity: sha512-kpbVlznPuxNQahssQvZiNPQo/iPme7qV3WMQeg6TYNCkYD7n6vEqeFZ5E/eQgB59xCanpvw4Ci8y/+SdMK6BUg==} - engines: {node: '>=16.13'} - '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -610,15 +523,9 @@ packages: cpu: [x64] os: [win32] - '@sinclair/typebox@0.27.8': - resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} - '@tsndr/cloudflare-worker-jwt@2.5.1': resolution: {integrity: sha512-MKB8jOdMcZZTeAovOBbAl2Z1PPmPiyceZUmCM0hlgHapQqOT1riQlvIFX6jOmfEpAhFGp0VaGAq995kr4mNMMg==} - '@types/better-sqlite3@7.6.9': - resolution: {integrity: sha512-FvktcujPDj9XKMJQWFcl2vVl7OdRIqsSRX9b0acWwTmwLK9CF2eqo/FRcmMLNpugKoX/avA6pb7TorDLmpgTnQ==} - '@types/estree@1.0.5': resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} @@ -631,30 +538,41 @@ packages: '@ungap/structured-clone@1.2.0': resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} - '@vitest/expect@1.3.1': - resolution: {integrity: sha512-xofQFwIzfdmLLlHa6ag0dPV8YsnKOCP1KdAeVVh34vSjN2dcUiXYCD9htu/9eM7t8Xln4v03U9HLxLpPlsXdZw==} + '@vitest/expect@2.1.1': + resolution: {integrity: sha512-YeueunS0HiHiQxk+KEOnq/QMzlUuOzbU1Go+PgAsHvvv3tUkJPm9xWt+6ITNTlzsMXUjmgm5T+U7KBPK2qQV6w==} + + '@vitest/mocker@2.1.1': + resolution: {integrity: sha512-LNN5VwOEdJqCmJ/2XJBywB11DLlkbY0ooDJW3uRX5cZyYCrc4PI/ePX0iQhE3BiEGiQmK4GE7Q/PqCkkaiPnrA==} + peerDependencies: + '@vitest/spy': 2.1.1 + msw: ^2.3.5 + vite: ^5.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@2.1.1': + resolution: {integrity: sha512-SjxPFOtuINDUW8/UkElJYQSFtnWX7tMksSGW0vfjxMneFqxVr8YJ979QpMbDW7g+BIiq88RAGDjf7en6rvLPPQ==} - '@vitest/runner@1.3.1': - resolution: {integrity: sha512-5FzF9c3jG/z5bgCnjr8j9LNq/9OxV2uEBAITOXfoe3rdZJTdO7jzThth7FXv/6b+kdY65tpRQB7WaKhNZwX+Kg==} + '@vitest/runner@2.1.1': + resolution: {integrity: sha512-uTPuY6PWOYitIkLPidaY5L3t0JJITdGTSwBtwMjKzo5O6RCOEncz9PUN+0pDidX8kTHYjO0EwUIvhlGpnGpxmA==} - '@vitest/snapshot@1.3.1': - resolution: {integrity: sha512-EF++BZbt6RZmOlE3SuTPu/NfwBF6q4ABS37HHXzs2LUVPBLx2QoY/K0fKpRChSo8eLiuxcbCVfqKgx/dplCDuQ==} + '@vitest/snapshot@2.1.1': + resolution: {integrity: sha512-BnSku1WFy7r4mm96ha2FzN99AZJgpZOWrAhtQfoxjUU5YMRpq1zmHRq7a5K9/NjqonebO7iVDla+VvZS8BOWMw==} - '@vitest/spy@1.3.1': - resolution: {integrity: sha512-xAcW+S099ylC9VLU7eZfdT9myV67Nor9w9zhf0mGCYJSO+zM2839tOeROTdikOi/8Qeusffvxb/MyBSOja1Uig==} + '@vitest/spy@2.1.1': + resolution: {integrity: sha512-ZM39BnZ9t/xZ/nF4UwRH5il0Sw93QnZXd9NAZGRpIgj0yvVwPpLd702s/Cx955rGaMlyBQkZJ2Ir7qyY48VZ+g==} - '@vitest/utils@1.3.1': - resolution: {integrity: sha512-d3Waie/299qqRyHTm2DjADeTaNdNSVsnwHPWrs20JMpjh6eiVq7ggggweO8rc4arhf6rRkWuHKwvxGvejUXZZQ==} + '@vitest/utils@2.1.1': + resolution: {integrity: sha512-Y6Q9TsI+qJ2CC0ZKj6VBb+T8UPz593N113nnUykqwANqhgf3QkZeHFlusgKLTqrnVHbj/XDKZcDHol+dxVT+rQ==} acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 - acorn-walk@8.3.2: - resolution: {integrity: sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==} - engines: {node: '>=0.4.0'} - acorn-walk@8.3.3: resolution: {integrity: sha512-MxXdReSRhGO7VlFe1bRG/oI7/mdLV9B9JJT0N8vZOhF7gFRR5l3M8W9G8JxmKV+JC5mGqJ0QvqfSOLsCPa4nUw==} engines: {node: '>=0.4.0'} @@ -680,10 +598,6 @@ packages: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} engines: {node: '>=8'} - ansi-styles@5.2.0: - resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} - engines: {node: '>=10'} - anymatch@3.1.3: resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} engines: {node: '>= 8'} @@ -694,8 +608,9 @@ packages: as-table@1.0.55: resolution: {integrity: sha512-xvsWESUJn0JN421Xb9MQw6AsMHRCUknCe0Wjlxvjud80mU4E6hQf1A6NzQKcYNmYw62MfzEtXc+badstZP3JpQ==} - assertion-error@1.1.0: - resolution: {integrity: sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==} + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} @@ -704,6 +619,9 @@ packages: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} engines: {node: '>=8'} + birpc@0.2.14: + resolution: {integrity: sha512-37FHE8rqsYM5JEKCnXFyHpBCzvgHEExwVVTq+nUmloInU7l8ezD1TpOhKpS8oe1DTYFqEK27rFZVKG43oTqXRA==} + blake3-wasm@2.1.5: resolution: {integrity: sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==} @@ -714,13 +632,6 @@ packages: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} - builtins@5.0.1: - resolution: {integrity: sha512-qwVpFEHNfhYJIzNRBvd2C1kyo6jz3ZSMPyyuR47OPdiKWlbYnZNyDWuyR175qDnAJLiCo5fBBqPb3RiXgWlkOQ==} - - busboy@1.6.0: - resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} - engines: {node: '>=10.16.0'} - cac@6.7.14: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} engines: {node: '>=8'} @@ -732,21 +643,25 @@ packages: capnp-ts@0.7.0: resolution: {integrity: sha512-XKxXAC3HVPv7r674zP0VC3RTXz+/JKhfyw94ljvF80yynK6VkTnqE3jMuN8b3dUVmmc43TjyxjW4KTsmB3c86g==} - chai@4.4.1: - resolution: {integrity: sha512-13sOfMv2+DWduEU+/xbun3LScLoqN17nBeTLUsmDfKdoiC1fr0n9PU4guu4AhRcOVFk/sW8LyZWHuhWtQZiF+g==} - engines: {node: '>=4'} + chai@5.1.1: + resolution: {integrity: sha512-pT1ZgP8rPNqUgieVaEY+ryQr6Q4HXNg8Ei9UnLUrjN4IA7dvQC5JB+/kxVcPNDHyBcc/26CXPkbNzq3qwrOEKA==} + engines: {node: '>=12'} chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} - check-error@1.0.3: - resolution: {integrity: sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==} + check-error@2.1.1: + resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==} + engines: {node: '>= 16'} chokidar@3.6.0: resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} engines: {node: '>= 8.10.0'} + cjs-module-lexer@1.4.1: + resolution: {integrity: sha512-cuSVIHi9/9E/+821Qjdvngor+xpnlwnuwIyZOaLmHBVdXL+gP+I6QQB9VkO7RI77YIcTV+S1W9AreJ5eN63JBA==} + color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} @@ -757,10 +672,6 @@ packages: concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} - consola@3.2.3: - resolution: {integrity: sha512-I5qxpzLv+sJhTVEoLYNcTW+bThDCPsit0vLNKShZx6rLtpilNpmmeTPaeqJb9ZE9dV3DGaeby6Vuhrw38WjeyQ==} - engines: {node: ^14.18.0 || >=16.10.0} - cookie@0.5.0: resolution: {integrity: sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==} engines: {node: '>= 0.6'} @@ -777,6 +688,9 @@ packages: data-uri-to-buffer@2.0.2: resolution: {integrity: sha512-ND9qDTLc6diwj+Xe5cdAgVTbLVdXbtxTJRXRhli8Mowuaan+0EJOtdqJ0QCHNSSPyoXGx9HX2/VMnKeC34AChA==} + date-fns@3.6.0: + resolution: {integrity: sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==} + debug@4.3.4: resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} engines: {node: '>=6.0'} @@ -786,8 +700,17 @@ packages: supports-color: optional: true - deep-eql@4.1.3: - resolution: {integrity: sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==} + debug@4.3.7: + resolution: {integrity: sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + deep-eql@5.0.2: + resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} engines: {node: '>=6'} deep-is@0.1.4: @@ -796,18 +719,13 @@ packages: defu@6.1.4: resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==} - diff-sequences@29.6.3: - resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + devalue@4.3.3: + resolution: {integrity: sha512-UH8EL6H2ifcY8TbD2QsxwCC/pr5xSwPvv85LrLXVihmHVC3T3YqTCIwnR5ak0yO1KYqlxrPVOA/JVZJYPy2ATg==} doctrine@3.0.0: resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} engines: {node: '>=6.0.0'} - dotenv@10.0.0: - resolution: {integrity: sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==} - engines: {node: '>=10'} - esbuild@0.17.19: resolution: {integrity: sha512-XQ0jAPFkK/u3LcVRcvVHQcTIqD6E2H1fvZMA5dQPSOWb3suUbWbfbRf94pjc0bNzRYLfIrDRQXr7X+LHIm5oHw==} engines: {node: '>=12'} @@ -861,14 +779,6 @@ packages: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} - execa@6.1.0: - resolution: {integrity: sha512-QVWlX2e50heYJcCPG0iWtf8r0xjEYfz/OYLGDYH+IyjWezzPNxz63qNFOu0l4YftGWuizFVZHHs8PrLU5p2IDA==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - - execa@8.0.1: - resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==} - engines: {node: '>=16.17'} - exit-hook@2.2.1: resolution: {integrity: sha512-eNTPlAD67BmP31LDINZ3U7HSF8l57TxOY2PmBJ1shpCvpnxBF93mWCE8YHBnXs8qiUZJc9WDcWIeC3a2HIAMfw==} engines: {node: '>=6'} @@ -921,14 +831,6 @@ packages: get-source@2.0.12: resolution: {integrity: sha512-X5+4+iD+HoSeEED+uwrQ07BOQr0kEDFMVqqpBuI+RaZBpBpHCuXxo70bjar6f0b0u/DQJsJ7ssurpP0V60Az+w==} - get-stream@6.0.1: - resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} - engines: {node: '>=10'} - - get-stream@8.0.1: - resolution: {integrity: sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==} - engines: {node: '>=16'} - glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} @@ -958,20 +860,6 @@ packages: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} - html-rewriter-wasm@0.4.1: - resolution: {integrity: sha512-lNovG8CMCCmcVB1Q7xggMSf7tqPCijZXaH4gL6iE8BFghdQCbaY5Met9i1x2Ex8m/cZHDUtXK9H6/znKamRP8Q==} - - http-cache-semantics@4.1.1: - resolution: {integrity: sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==} - - human-signals@3.0.1: - resolution: {integrity: sha512-rQLskxnM/5OCldHo+wNXbpVgDn5A17CUoKX+7Sokwaknlq7CdSnphy0W39GU8dw59XiCXmFXDg4fRuckQRKewQ==} - engines: {node: '>=12.20.0'} - - human-signals@5.0.0: - resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==} - engines: {node: '>=16.17.0'} - ignore@5.3.1: resolution: {integrity: sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==} engines: {node: '>= 4'} @@ -1013,19 +901,12 @@ packages: resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} engines: {node: '>=8'} - is-stream@3.0.0: - resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} itty-router@4.0.27: resolution: {integrity: sha512-Q3/GOE2EJvyu3hhxGN3WDWh3QNg4v7h1KFx/jSLcIOOkpSI1jUFTgGefEESXon4j5YwqCIf0DEemjiVAFSBiUw==} - js-tokens@8.0.3: - resolution: {integrity: sha512-UfJMcSJc+SEXEl9lH/VLHSZbThQyLpw1vLO1Lb+j4RWDvG3N2f7yj3PVQA3cmkTBNldJ9eFnM+xEXxHIXrYiJw==} - js-yaml@4.1.0: resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} hasBin: true @@ -1039,24 +920,13 @@ packages: json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} - jsonc-parser@3.2.1: - resolution: {integrity: sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA==} - keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} - kleur@4.1.5: - resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} - engines: {node: '>=6'} - levn@0.4.1: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} - local-pkg@0.5.0: - resolution: {integrity: sha512-ok6z3qlYyCDS4ZEU27HaU6x/xZa9Whf8jD4ptH5UZTQYZVYeb9bnZ3ojVhiJNLiXK1Hfc0GNbLXcmZ5plLDDBg==} - engines: {node: '>=14'} - locate-path@6.0.0: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} @@ -1064,51 +934,34 @@ packages: lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} - loupe@2.3.7: - resolution: {integrity: sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==} - - lru-cache@6.0.0: - resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} - engines: {node: '>=10'} + loupe@3.1.1: + resolution: {integrity: sha512-edNu/8D5MKVfGVFRhFf8aAxiTM6Wumfz5XsaatSxlD3w4R1d/WEKUTydCdPGbl9K7QG/Ca3GnDV2sIKIpXRQcw==} magic-string@0.25.9: resolution: {integrity: sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==} - magic-string@0.30.7: - resolution: {integrity: sha512-8vBuFF/I/+OSLRmdf2wwFCJCz+nSn0m6DPvGH1fS/KiQoSaR+sETbov0eIk9KhEKy8CYqIkIAnbohxT/4H0kuA==} - engines: {node: '>=12'} - - merge-stream@2.0.0: - resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} + magic-string@0.30.11: + resolution: {integrity: sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==} mime@3.0.0: resolution: {integrity: sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==} engines: {node: '>=10.0.0'} hasBin: true - mimic-fn@4.0.0: - resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==} - engines: {node: '>=12'} - - miniflare@3.20240208.0: - resolution: {integrity: sha512-NnP3MQFh2pV7iETNmJzSlMBF/KhRA+XT4A7JLCfxunadQSPbTMMgbsZo9EfLloMwHMUhZGNVot3Pvh+VnT2joQ==} - engines: {node: '>=16.13'} - hasBin: true - - miniflare@3.20240610.1: - resolution: {integrity: sha512-ZkfSpBmX3nJW00yYhvF2kGvjb6f77TOimRR6+2GQvsArbwo6e0iYqLGM9aB/cnJzgFjLMvOv1qj4756iynSxJQ==} + miniflare@3.20240909.4: + resolution: {integrity: sha512-uiMjmv9vYIMgUn5PovS/2XzvnSgm04GxtoreNb7qiaDdp1YMhPPtnmV+EKOKyPSlVc7fCt/glzqSX9atUBXa2A==} engines: {node: '>=16.13'} hasBin: true minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} - mlly@1.6.1: - resolution: {integrity: sha512-vLgaHvaeunuOXHSmEbZ9izxPx3USsk8KCQ8iC+aTlp5sKRSoZvwhHh5L9VbKSaVC6sJDqbyohIS76E2VmHIPAA==} - ms@2.1.2: resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + mustache@4.2.0: resolution: {integrity: sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==} hasBin: true @@ -1121,9 +974,6 @@ packages: natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} - node-fetch-native@1.6.4: - resolution: {integrity: sha512-IhOigYzAKHd244OC0JIMIUrjzctirCmPkaIfhDeGcEETWof5zKYUW7e7MYvChGWh/4CJeXEgsRyGzuF334rOOQ==} - node-forge@1.3.1: resolution: {integrity: sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==} engines: {node: '>= 6.13.0'} @@ -1132,20 +982,12 @@ packages: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} engines: {node: '>=0.10.0'} - npm-run-path@5.3.0: - resolution: {integrity: sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - - npx-import@1.1.4: - resolution: {integrity: sha512-3ShymTWOgqGyNlh5lMJAejLuIv3W1K3fbI5Ewc6YErZU3Sp0PqsNs8UIU1O8z5+KVl/Du5ag56Gza9vdorGEoA==} + ohash@1.1.4: + resolution: {integrity: sha512-FlDryZAahJmEF3VR3w1KogSEdWX3WhA5GPakFx4J81kEAiHyLMpdLLElS8n8dfNadMgAne/MywcvmogzscVt4g==} once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} - onetime@6.0.0: - resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==} - engines: {node: '>=12'} - optionator@0.9.3: resolution: {integrity: sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==} engines: {node: '>= 0.8.0'} @@ -1154,10 +996,6 @@ packages: resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} engines: {node: '>=10'} - p-limit@5.0.0: - resolution: {integrity: sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==} - engines: {node: '>=18'} - p-locate@5.0.0: resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} engines: {node: '>=10'} @@ -1166,9 +1004,6 @@ packages: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} - parse-package-name@1.0.0: - resolution: {integrity: sha512-kBeTUtcj+SkyfaW4+KBe0HtsloBJ/mKTPoxpVdA57GZiPerREsUWJOhVj9anXweFiJkm5y8FG1sxFZkZ0SN6wg==} - path-exists@4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} @@ -1181,21 +1016,18 @@ packages: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} - path-key@4.0.0: - resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} - engines: {node: '>=12'} - path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} - path-to-regexp@6.2.2: - resolution: {integrity: sha512-GQX3SSMokngb36+whdpRXE+3f9V8UzyAorlYvOGx87ufGHehNTn5lCxrKtLyZ4Yl/wEKnNnr98ZzOwwDZV5ogw==} + path-to-regexp@6.3.0: + resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} pathe@1.1.2: resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} - pathval@1.1.1: - resolution: {integrity: sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==} + pathval@2.0.0: + resolution: {integrity: sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==} + engines: {node: '>= 14.16'} picocolors@1.0.0: resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} @@ -1204,9 +1036,6 @@ packages: resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} engines: {node: '>=8.6'} - pkg-types@1.0.3: - resolution: {integrity: sha512-nN7pYi0AQqJnoLPC9eHFQ8AcyaixBUOwvqc5TDnIKCMEE6I0y8P7OKA7fPexsXGCGxQDl/cmrLAp26LhcwxZ4A==} - postcss@8.4.38: resolution: {integrity: sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==} engines: {node: ^10 || ^12 || >=14} @@ -1215,10 +1044,6 @@ packages: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} - pretty-format@29.7.0: - resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - printable-characters@1.0.42: resolution: {integrity: sha512-dKp+C4iXWK4vVYZmYSd0KBH5F/h1HoZRsbJ82AVKRO3PEo8L4lBS/vLwhVtpwwuYcoIsVY+1JYKR268yn480uQ==} @@ -1229,9 +1054,6 @@ packages: queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} - react-is@18.2.0: - resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==} - readdirp@3.6.0: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} engines: {node: '>=8.10.0'} @@ -1281,14 +1103,11 @@ packages: resolution: {integrity: sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q==} engines: {node: '>=10'} - semver@7.6.0: - resolution: {integrity: sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==} + semver@7.6.3: + resolution: {integrity: sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==} engines: {node: '>=10'} hasBin: true - set-cookie-parser@2.6.0: - resolution: {integrity: sha512-RVnVQxTXuerk653XfuliOxBP81Sf0+qfQE73LIYKcyMYHG94AuH0kgrQpRDuTZnSmjpysHmzxJXKNfa6PjFhyQ==} - shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -1300,13 +1119,6 @@ packages: siginfo@2.0.0: resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} - signal-exit@3.0.7: - resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} - - signal-exit@4.1.0: - resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} - engines: {node: '>=14'} - source-map-js@1.2.0: resolution: {integrity: sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==} engines: {node: '>=0.10.0'} @@ -1332,25 +1144,14 @@ packages: resolution: {integrity: sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==} engines: {node: '>=4', npm: '>=6'} - streamsearch@1.1.0: - resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} - engines: {node: '>=10.0.0'} - strip-ansi@6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} - strip-final-newline@3.0.0: - resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==} - engines: {node: '>=12'} - strip-json-comments@3.1.1: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} - strip-literal@2.0.0: - resolution: {integrity: sha512-f9vHgsCWBq2ugHAkGMiiYY+AYG0D/cbloKKg0nhaaaSNsujdGIpVXCNsrJpCKr5M0f4aI31mr13UjY6GAuXCKA==} - supports-color@7.2.0: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} @@ -1362,15 +1163,22 @@ packages: text-table@0.2.0: resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} - tinybench@2.6.0: - resolution: {integrity: sha512-N8hW3PG/3aOoZAN5V/NSAEDz0ZixDSSt5b/a05iqtpgfLWMSVuCo7w0k2vVvEjdrIoeGqZzweX2WlyioNIHchA==} + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@0.3.0: + resolution: {integrity: sha512-tVGE0mVJPGb0chKhqmsoosjsS+qUnJVGJpZgsHYQcGoPlG3B51R3PouqTgEGH2Dc9jjFyOqOpix6ZHNMXp1FZg==} + + tinypool@1.0.1: + resolution: {integrity: sha512-URZYihUbRPcGv95En+sz6MfghfIc2OJ1sv/RmhWZLouPY0/8Vo80viwPvg3dlaS9fuq7fQMEfgRRK7BBZThBEA==} + engines: {node: ^18.0.0 || >=20.0.0} - tinypool@0.8.2: - resolution: {integrity: sha512-SUszKYe5wgsxnNOVlBYO6IC+8VGWdVGZWAqUxp3UErNBtptZvWbwyUOyzNL59zigz2rCA92QiL3wvG+JDSdJdQ==} + tinyrainbow@1.2.0: + resolution: {integrity: sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==} engines: {node: '>=14.0.0'} - tinyspy@2.2.1: - resolution: {integrity: sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==} + tinyspy@3.0.2: + resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==} engines: {node: '>=14.0.0'} to-regex-range@5.0.1: @@ -1384,10 +1192,6 @@ packages: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} - type-detect@4.0.8: - resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==} - engines: {node: '>=4'} - type-fest@0.20.2: resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} engines: {node: '>=10'} @@ -1397,39 +1201,21 @@ packages: engines: {node: '>=14.17'} hasBin: true - ufo@1.4.0: - resolution: {integrity: sha512-Hhy+BhRBleFjpJ2vchUNN40qgkh0366FWJGqVLYBHev0vpHTrXSA0ryT+74UiW6KWsldNurQMKGqCm1M2zBciQ==} - - ufo@1.5.3: - resolution: {integrity: sha512-Y7HYmWaFwPUmkoQCUIAYpKqkOf+SbVj/2fJJZ4RJMCfZp0rTGwRbzQD+HghfnhKOjL9E01okqz+ncJskGYfBNw==} - - undici@5.28.2: - resolution: {integrity: sha512-wh1pHJHnUeQV5Xa8/kyQhO7WFa8M34l026L5P/+2TYiakvGy5Rdc8jWZVyG7ieht/0WgJLEd3kcU5gKx+6GC8w==} - engines: {node: '>=14.0'} - - undici@5.28.3: - resolution: {integrity: sha512-3ItfzbrhDlINjaP0duwnNsKpDQk3acHI3gVJ1z4fmwMK31k5G9OVIAMLSIaP6w4FaGkaAkN6zaQO9LUvZ1t7VA==} - engines: {node: '>=14.0'} + ufo@1.5.4: + resolution: {integrity: sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ==} undici@5.28.4: resolution: {integrity: sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==} engines: {node: '>=14.0'} - unenv-nightly@1.10.0-1717606461.a117952: - resolution: {integrity: sha512-u3TfBX02WzbHTpaEfWEKwDijDSFAHcgXkayUZ+MVDrjhLFvgAJzFGTSTmwlEhwWi2exyRQey23ah9wELMM6etg==} + unenv-nightly@2.0.0-20240919-125358-9a64854: + resolution: {integrity: sha512-XjsgUTrTHR7iw+k/SRTNjh6EQgwpC9voygnoCJo5kh4hKqsSDHUW84MhL9EsHTNfLctvVBHaSw8e2k3R2fKXsQ==} uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} - urlpattern-polyfill@4.0.3: - resolution: {integrity: sha512-DOE84vZT2fEcl9gqCUTcnAw5ZY5Id55ikUcziSUntuEFL3pRvavg5kwDmTEUJkeCHInTlV/HexFomgYnzO5kdQ==} - - validate-npm-package-name@4.0.0: - resolution: {integrity: sha512-mzR0L8ZDktZjpX4OB46KT+56MAhl4EIazWP/+G/HPGuvfdaqg4YsCdtOm6U9+LOFyYDoh4dpnpxZRB9MQQns5Q==} - engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} - - vite-node@1.3.1: - resolution: {integrity: sha512-azbRrqRxlWTJEVbzInZCTchx0X69M/XPTCz4H+TLvlTcR/xH/3hkRqhOakT41fMJCMzXTu4UvegkZiEoJAWvng==} + vite-node@2.1.1: + resolution: {integrity: sha512-N/mGckI1suG/5wQI35XeR9rsMsPqKXzq1CdUndzVstBj/HvyxxGctwnK6WX43NGt5L3Z5tcRf83g4TITKJhPrA==} engines: {node: ^18.0.0 || >=20.0.0} hasBin: true @@ -1461,21 +1247,15 @@ packages: terser: optional: true - vitest-environment-miniflare@2.14.2: - resolution: {integrity: sha512-9bVoJ59m8RtW+KxXoQBTm+2ljZDTuN/yxth3VlIJV4oOpjLo7ambK+exWJmoU+2lcSf0WAC/610a3gJuAJJDTg==} - engines: {node: '>=16.13'} - peerDependencies: - vitest: '>=0.23.0' - - vitest@1.3.1: - resolution: {integrity: sha512-/1QJqXs8YbCrfv/GPQ05wAZf2eakUPLPa18vkJAKE7RXOKfVHqMZZ1WlTjiwl6Gcn65M5vpNUB6EFLnEdRdEXQ==} + vitest@2.1.1: + resolution: {integrity: sha512-97We7/VC0e9X5zBVkvt7SGQMGrRtn3KtySFQG5fpaMlS+l62eeXRQO633AYhSTC3z7IMebnPPNjGXVGNRFlxBA==} engines: {node: ^18.0.0 || >=20.0.0} hasBin: true peerDependencies: '@edge-runtime/vm': '*' '@types/node': 18.15.3 - '@vitest/browser': 1.3.1 - '@vitest/ui': 1.3.1 + '@vitest/browser': 2.1.1 + '@vitest/ui': 2.1.1 happy-dom: '*' jsdom: '*' peerDependenciesMeta: @@ -1497,27 +1277,22 @@ packages: engines: {node: '>= 8'} hasBin: true - why-is-node-running@2.2.2: - resolution: {integrity: sha512-6tSwToZxTOcotxHeA+qGCq1mVzKR3CwcJGmVcY+QE8SHy6TnpFnh8PAvPNHYr7EcuVeG0QSMxtYCuO1ta/G/oA==} + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} engines: {node: '>=8'} hasBin: true - workerd@1.20240208.0: - resolution: {integrity: sha512-edFdwHU95Ww2SmjBvBJhbc7hhVXMEo6Y7qqSWCl6W9lGScTlCMCXd4AU3f/EGJ3P++FC+CWqu+XuAywebbKF2Q==} + workerd@1.20240909.0: + resolution: {integrity: sha512-NwuYh/Fgr/MK0H+Ht687sHl/f8tumwT5CWzYR0MZMHri8m3CIYu2IaY4tBFWoKE/tOU1Z5XjEXECa9zXY4+lwg==} engines: {node: '>=16'} hasBin: true - workerd@1.20240610.1: - resolution: {integrity: sha512-Rtut5GrsODQMh6YU43b9WZ980Wd05Ov1/ds88pT/SoetmXFBvkBzdRfiHiATv+azmGX8KveE0i/Eqzk/yI01ug==} - engines: {node: '>=16'} - hasBin: true - - wrangler@3.61.0: - resolution: {integrity: sha512-feVAp0986x9xL3Dc1zin0ZVXKaqzp7eZur7iPLnpEwjG1Xy4dkVEZ5a1LET94Iyejt1P+EX5lgGcz63H7EfzUw==} + wrangler@3.78.7: + resolution: {integrity: sha512-z2ubdgQZ8lh2TEpvihFQOu3HmCNus78sC1LMBiSmgv133i4DeUMuz6SJglles2LayJAKrusjTqFnDYecA2XDDg==} engines: {node: '>=16.17.0'} hasBin: true peerDependencies: - '@cloudflare/workers-types': ^4.20240605.0 + '@cloudflare/workers-types': ^4.20240909.0 peerDependenciesMeta: '@cloudflare/workers-types': optional: true @@ -1525,18 +1300,6 @@ packages: wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} - ws@8.16.0: - resolution: {integrity: sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==} - engines: {node: '>=10.0.0'} - peerDependencies: - bufferutil: ^4.0.1 - utf-8-validate: '>=5.0.2' - peerDependenciesMeta: - bufferutil: - optional: true - utf-8-validate: - optional: true - ws@8.17.1: resolution: {integrity: sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==} engines: {node: '>=10.0.0'} @@ -1552,17 +1315,10 @@ packages: xxhash-wasm@1.0.2: resolution: {integrity: sha512-ibF0Or+FivM9lNrg+HGJfVX8WJqgo+kCLDc4vx6xMeTce7Aj+DLttKbxxRR/gNLSAelRc1omAPlJ77N/Jem07A==} - yallist@4.0.0: - resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} - yocto-queue@0.1.0: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} - yocto-queue@1.0.0: - resolution: {integrity: sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==} - engines: {node: '>=12.20'} - youch@3.3.3: resolution: {integrity: sha512-qSFXUk3UZBLfggAW3dJKg0BMblG5biqSF8M34E06o5CSsZtH92u9Hqmj2RzGiHDi64fhe83+4tENFP2DB6t6ZA==} @@ -1577,39 +1333,48 @@ snapshots: dependencies: rfc4648: 1.5.3 - '@cloudflare/kv-asset-handler@0.3.3': + '@cloudflare/kv-asset-handler@0.3.4': dependencies: mime: 3.0.0 - '@cloudflare/workerd-darwin-64@1.20240208.0': - optional: true - - '@cloudflare/workerd-darwin-64@1.20240610.1': - optional: true - - '@cloudflare/workerd-darwin-arm64@1.20240208.0': - optional: true - - '@cloudflare/workerd-darwin-arm64@1.20240610.1': - optional: true + '@cloudflare/vitest-pool-workers@0.5.7(@cloudflare/workers-types@4.20240614.0)(@vitest/runner@2.1.1)(@vitest/snapshot@2.1.1)(vitest@2.1.1(@types/node@18.15.3))': + dependencies: + '@vitest/runner': 2.1.1 + '@vitest/snapshot': 2.1.1 + birpc: 0.2.14 + cjs-module-lexer: 1.4.1 + devalue: 4.3.3 + esbuild: 0.17.19 + miniflare: 3.20240909.4 + semver: 7.6.3 + vitest: 2.1.1(@types/node@18.15.3) + wrangler: 3.78.7(@cloudflare/workers-types@4.20240614.0) + zod: 3.22.4 + transitivePeerDependencies: + - '@cloudflare/workers-types' + - bufferutil + - supports-color + - utf-8-validate - '@cloudflare/workerd-linux-64@1.20240208.0': + '@cloudflare/workerd-darwin-64@1.20240909.0': optional: true - '@cloudflare/workerd-linux-64@1.20240610.1': + '@cloudflare/workerd-darwin-arm64@1.20240909.0': optional: true - '@cloudflare/workerd-linux-arm64@1.20240208.0': + '@cloudflare/workerd-linux-64@1.20240909.0': optional: true - '@cloudflare/workerd-linux-arm64@1.20240610.1': + '@cloudflare/workerd-linux-arm64@1.20240909.0': optional: true - '@cloudflare/workerd-windows-64@1.20240208.0': + '@cloudflare/workerd-windows-64@1.20240909.0': optional: true - '@cloudflare/workerd-windows-64@1.20240610.1': - optional: true + '@cloudflare/workers-shared@0.5.3': + dependencies: + mime: 3.0.0 + zod: 3.22.4 '@cloudflare/workers-types@4.20240614.0': {} @@ -1799,133 +1564,17 @@ snapshots: '@humanwhocodes/object-schema@2.0.2': {} - '@iarna/toml@2.2.5': {} - - '@jest/schemas@29.6.3': - dependencies: - '@sinclair/typebox': 0.27.8 - '@jridgewell/resolve-uri@3.1.2': {} '@jridgewell/sourcemap-codec@1.4.15': {} + '@jridgewell/sourcemap-codec@1.5.0': {} + '@jridgewell/trace-mapping@0.3.9': dependencies: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.4.15 - '@miniflare/cache@2.14.2': - dependencies: - '@miniflare/core': 2.14.2 - '@miniflare/shared': 2.14.2 - http-cache-semantics: 4.1.1 - undici: 5.28.2 - - '@miniflare/core@2.14.2': - dependencies: - '@iarna/toml': 2.2.5 - '@miniflare/queues': 2.14.2 - '@miniflare/shared': 2.14.2 - '@miniflare/watcher': 2.14.2 - busboy: 1.6.0 - dotenv: 10.0.0 - kleur: 4.1.5 - set-cookie-parser: 2.6.0 - undici: 5.28.2 - urlpattern-polyfill: 4.0.3 - - '@miniflare/d1@2.14.2': - dependencies: - '@miniflare/core': 2.14.2 - '@miniflare/shared': 2.14.2 - - '@miniflare/durable-objects@2.14.2': - dependencies: - '@miniflare/core': 2.14.2 - '@miniflare/shared': 2.14.2 - '@miniflare/storage-memory': 2.14.2 - undici: 5.28.2 - - '@miniflare/html-rewriter@2.14.2': - dependencies: - '@miniflare/core': 2.14.2 - '@miniflare/shared': 2.14.2 - html-rewriter-wasm: 0.4.1 - undici: 5.28.2 - - '@miniflare/kv@2.14.2': - dependencies: - '@miniflare/shared': 2.14.2 - - '@miniflare/queues@2.14.2': - dependencies: - '@miniflare/shared': 2.14.2 - - '@miniflare/r2@2.14.2': - dependencies: - '@miniflare/core': 2.14.2 - '@miniflare/shared': 2.14.2 - undici: 5.28.2 - - '@miniflare/runner-vm@2.14.2': - dependencies: - '@miniflare/shared': 2.14.2 - - '@miniflare/shared-test-environment@2.14.2': - dependencies: - '@cloudflare/workers-types': 4.20240614.0 - '@miniflare/cache': 2.14.2 - '@miniflare/core': 2.14.2 - '@miniflare/d1': 2.14.2 - '@miniflare/durable-objects': 2.14.2 - '@miniflare/html-rewriter': 2.14.2 - '@miniflare/kv': 2.14.2 - '@miniflare/queues': 2.14.2 - '@miniflare/r2': 2.14.2 - '@miniflare/shared': 2.14.2 - '@miniflare/sites': 2.14.2 - '@miniflare/storage-memory': 2.14.2 - '@miniflare/web-sockets': 2.14.2 - transitivePeerDependencies: - - bufferutil - - utf-8-validate - - '@miniflare/shared@2.14.2': - dependencies: - '@types/better-sqlite3': 7.6.9 - kleur: 4.1.5 - npx-import: 1.1.4 - picomatch: 2.3.1 - - '@miniflare/sites@2.14.2': - dependencies: - '@miniflare/kv': 2.14.2 - '@miniflare/shared': 2.14.2 - '@miniflare/storage-file': 2.14.2 - - '@miniflare/storage-file@2.14.2': - dependencies: - '@miniflare/shared': 2.14.2 - '@miniflare/storage-memory': 2.14.2 - - '@miniflare/storage-memory@2.14.2': - dependencies: - '@miniflare/shared': 2.14.2 - - '@miniflare/watcher@2.14.2': - dependencies: - '@miniflare/shared': 2.14.2 - - '@miniflare/web-sockets@2.14.2': - dependencies: - '@miniflare/core': 2.14.2 - '@miniflare/shared': 2.14.2 - undici: 5.28.2 - ws: 8.16.0 - transitivePeerDependencies: - - bufferutil - - utf-8-validate - '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 @@ -1983,14 +1632,8 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.14.0': optional: true - '@sinclair/typebox@0.27.8': {} - '@tsndr/cloudflare-worker-jwt@2.5.1': {} - '@types/better-sqlite3@7.6.9': - dependencies: - '@types/node': 18.15.3 - '@types/estree@1.0.5': {} '@types/node-forge@1.3.11': @@ -2001,41 +1644,50 @@ snapshots: '@ungap/structured-clone@1.2.0': {} - '@vitest/expect@1.3.1': + '@vitest/expect@2.1.1': dependencies: - '@vitest/spy': 1.3.1 - '@vitest/utils': 1.3.1 - chai: 4.4.1 + '@vitest/spy': 2.1.1 + '@vitest/utils': 2.1.1 + chai: 5.1.1 + tinyrainbow: 1.2.0 - '@vitest/runner@1.3.1': + '@vitest/mocker@2.1.1(@vitest/spy@2.1.1)(vite@5.2.8(@types/node@18.15.3))': dependencies: - '@vitest/utils': 1.3.1 - p-limit: 5.0.0 + '@vitest/spy': 2.1.1 + estree-walker: 3.0.3 + magic-string: 0.30.11 + optionalDependencies: + vite: 5.2.8(@types/node@18.15.3) + + '@vitest/pretty-format@2.1.1': + dependencies: + tinyrainbow: 1.2.0 + + '@vitest/runner@2.1.1': + dependencies: + '@vitest/utils': 2.1.1 pathe: 1.1.2 - '@vitest/snapshot@1.3.1': + '@vitest/snapshot@2.1.1': dependencies: - magic-string: 0.30.7 + '@vitest/pretty-format': 2.1.1 + magic-string: 0.30.11 pathe: 1.1.2 - pretty-format: 29.7.0 - '@vitest/spy@1.3.1': + '@vitest/spy@2.1.1': dependencies: - tinyspy: 2.2.1 + tinyspy: 3.0.2 - '@vitest/utils@1.3.1': + '@vitest/utils@2.1.1': dependencies: - diff-sequences: 29.6.3 - estree-walker: 3.0.3 - loupe: 2.3.7 - pretty-format: 29.7.0 + '@vitest/pretty-format': 2.1.1 + loupe: 3.1.1 + tinyrainbow: 1.2.0 acorn-jsx@5.3.2(acorn@8.11.3): dependencies: acorn: 8.11.3 - acorn-walk@8.3.2: {} - acorn-walk@8.3.3: dependencies: acorn: 8.12.0 @@ -2057,8 +1709,6 @@ snapshots: dependencies: color-convert: 2.0.1 - ansi-styles@5.2.0: {} - anymatch@3.1.3: dependencies: normalize-path: 3.0.0 @@ -2070,12 +1720,14 @@ snapshots: dependencies: printable-characters: 1.0.42 - assertion-error@1.1.0: {} + assertion-error@2.0.1: {} balanced-match@1.0.2: {} binary-extensions@2.3.0: {} + birpc@0.2.14: {} + blake3-wasm@2.1.5: {} brace-expansion@1.1.11: @@ -2087,14 +1739,6 @@ snapshots: dependencies: fill-range: 7.1.1 - builtins@5.0.1: - dependencies: - semver: 7.6.0 - - busboy@1.6.0: - dependencies: - streamsearch: 1.1.0 - cac@6.7.14: {} callsites@3.1.0: {} @@ -2106,24 +1750,20 @@ snapshots: transitivePeerDependencies: - supports-color - chai@4.4.1: + chai@5.1.1: dependencies: - assertion-error: 1.1.0 - check-error: 1.0.3 - deep-eql: 4.1.3 - get-func-name: 2.0.2 - loupe: 2.3.7 - pathval: 1.1.1 - type-detect: 4.0.8 + assertion-error: 2.0.1 + check-error: 2.1.1 + deep-eql: 5.0.2 + loupe: 3.1.1 + pathval: 2.0.0 chalk@4.1.2: dependencies: ansi-styles: 4.3.0 supports-color: 7.2.0 - check-error@1.0.3: - dependencies: - get-func-name: 2.0.2 + check-error@2.1.1: {} chokidar@3.6.0: dependencies: @@ -2137,6 +1777,8 @@ snapshots: optionalDependencies: fsevents: 2.3.3 + cjs-module-lexer@1.4.1: {} + color-convert@2.0.1: dependencies: color-name: 1.1.4 @@ -2145,8 +1787,6 @@ snapshots: concat-map@0.0.1: {} - consola@3.2.3: {} - cookie@0.5.0: {} cross-env@7.0.3: @@ -2161,26 +1801,28 @@ snapshots: data-uri-to-buffer@2.0.2: {} + date-fns@3.6.0: {} + debug@4.3.4: dependencies: ms: 2.1.2 - deep-eql@4.1.3: + debug@4.3.7: dependencies: - type-detect: 4.0.8 + ms: 2.1.3 + + deep-eql@5.0.2: {} deep-is@0.1.4: {} defu@6.1.4: {} - diff-sequences@29.6.3: {} + devalue@4.3.3: {} doctrine@3.0.0: dependencies: esutils: 2.0.3 - dotenv@10.0.0: {} - esbuild@0.17.19: optionalDependencies: '@esbuild/android-arm': 0.17.19 @@ -2308,30 +1950,6 @@ snapshots: esutils@2.0.3: {} - execa@6.1.0: - dependencies: - cross-spawn: 7.0.3 - get-stream: 6.0.1 - human-signals: 3.0.1 - is-stream: 3.0.0 - merge-stream: 2.0.0 - npm-run-path: 5.3.0 - onetime: 6.0.0 - signal-exit: 3.0.7 - strip-final-newline: 3.0.0 - - execa@8.0.1: - dependencies: - cross-spawn: 7.0.3 - get-stream: 8.0.1 - human-signals: 5.0.0 - is-stream: 3.0.0 - merge-stream: 2.0.0 - npm-run-path: 5.3.0 - onetime: 6.0.0 - signal-exit: 4.1.0 - strip-final-newline: 3.0.0 - exit-hook@2.2.1: {} fast-deep-equal@3.1.3: {} @@ -2379,10 +1997,6 @@ snapshots: data-uri-to-buffer: 2.0.2 source-map: 0.6.1 - get-stream@6.0.1: {} - - get-stream@8.0.1: {} - glob-parent@5.1.2: dependencies: is-glob: 4.0.3 @@ -2414,14 +2028,6 @@ snapshots: dependencies: function-bind: 1.1.2 - html-rewriter-wasm@0.4.1: {} - - http-cache-semantics@4.1.1: {} - - human-signals@3.0.1: {} - - human-signals@5.0.0: {} - ignore@5.3.1: {} import-fresh@3.3.0: @@ -2456,14 +2062,10 @@ snapshots: is-path-inside@3.0.3: {} - is-stream@3.0.0: {} - isexe@2.0.0: {} itty-router@4.0.27: {} - js-tokens@8.0.3: {} - js-yaml@4.1.0: dependencies: argparse: 2.0.1 @@ -2474,72 +2076,36 @@ snapshots: json-stable-stringify-without-jsonify@1.0.1: {} - jsonc-parser@3.2.1: {} - keyv@4.5.4: dependencies: json-buffer: 3.0.1 - kleur@4.1.5: {} - levn@0.4.1: dependencies: prelude-ls: 1.2.1 type-check: 0.4.0 - local-pkg@0.5.0: - dependencies: - mlly: 1.6.1 - pkg-types: 1.0.3 - locate-path@6.0.0: dependencies: p-locate: 5.0.0 lodash.merge@4.6.2: {} - loupe@2.3.7: + loupe@3.1.1: dependencies: get-func-name: 2.0.2 - lru-cache@6.0.0: - dependencies: - yallist: 4.0.0 - magic-string@0.25.9: dependencies: sourcemap-codec: 1.4.8 - magic-string@0.30.7: + magic-string@0.30.11: dependencies: - '@jridgewell/sourcemap-codec': 1.4.15 - - merge-stream@2.0.0: {} + '@jridgewell/sourcemap-codec': 1.5.0 mime@3.0.0: {} - mimic-fn@4.0.0: {} - - miniflare@3.20240208.0: - dependencies: - '@cspotcode/source-map-support': 0.8.1 - acorn: 8.11.3 - acorn-walk: 8.3.2 - capnp-ts: 0.7.0 - exit-hook: 2.2.1 - glob-to-regexp: 0.4.1 - stoppable: 1.1.0 - undici: 5.28.3 - workerd: 1.20240208.0 - ws: 8.16.0 - youch: 3.3.3 - zod: 3.22.4 - transitivePeerDependencies: - - bufferutil - - supports-color - - utf-8-validate - - miniflare@3.20240610.1: + miniflare@3.20240909.4: dependencies: '@cspotcode/source-map-support': 0.8.1 acorn: 8.12.0 @@ -2549,7 +2115,7 @@ snapshots: glob-to-regexp: 0.4.1 stoppable: 1.1.0 undici: 5.28.4 - workerd: 1.20240610.1 + workerd: 1.20240909.0 ws: 8.17.1 youch: 3.3.3 zod: 3.22.4 @@ -2562,46 +2128,26 @@ snapshots: dependencies: brace-expansion: 1.1.11 - mlly@1.6.1: - dependencies: - acorn: 8.11.3 - pathe: 1.1.2 - pkg-types: 1.0.3 - ufo: 1.4.0 - ms@2.1.2: {} + ms@2.1.3: {} + mustache@4.2.0: {} nanoid@3.3.7: {} natural-compare@1.4.0: {} - node-fetch-native@1.6.4: {} - node-forge@1.3.1: {} normalize-path@3.0.0: {} - npm-run-path@5.3.0: - dependencies: - path-key: 4.0.0 - - npx-import@1.1.4: - dependencies: - execa: 6.1.0 - parse-package-name: 1.0.0 - semver: 7.6.0 - validate-npm-package-name: 4.0.0 + ohash@1.1.4: {} once@1.4.0: dependencies: wrappy: 1.0.2 - onetime@6.0.0: - dependencies: - mimic-fn: 4.0.0 - optionator@0.9.3: dependencies: '@aashutoshrathi/word-wrap': 1.2.6 @@ -2615,10 +2161,6 @@ snapshots: dependencies: yocto-queue: 0.1.0 - p-limit@5.0.0: - dependencies: - yocto-queue: 1.0.0 - p-locate@5.0.0: dependencies: p-limit: 3.1.0 @@ -2627,34 +2169,24 @@ snapshots: dependencies: callsites: 3.1.0 - parse-package-name@1.0.0: {} - path-exists@4.0.0: {} path-is-absolute@1.0.1: {} path-key@3.1.1: {} - path-key@4.0.0: {} - path-parse@1.0.7: {} - path-to-regexp@6.2.2: {} + path-to-regexp@6.3.0: {} pathe@1.1.2: {} - pathval@1.1.1: {} + pathval@2.0.0: {} picocolors@1.0.0: {} picomatch@2.3.1: {} - pkg-types@1.0.3: - dependencies: - jsonc-parser: 3.2.1 - mlly: 1.6.1 - pathe: 1.1.2 - postcss@8.4.38: dependencies: nanoid: 3.3.7 @@ -2663,20 +2195,12 @@ snapshots: prelude-ls@1.2.1: {} - pretty-format@29.7.0: - dependencies: - '@jest/schemas': 29.6.3 - ansi-styles: 5.2.0 - react-is: 18.2.0 - printable-characters@1.0.42: {} punycode@2.3.1: {} queue-microtask@1.2.3: {} - react-is@18.2.0: {} - readdirp@3.6.0: dependencies: picomatch: 2.3.1 @@ -2743,11 +2267,7 @@ snapshots: '@types/node-forge': 1.3.11 node-forge: 1.3.1 - semver@7.6.0: - dependencies: - lru-cache: 6.0.0 - - set-cookie-parser@2.6.0: {} + semver@7.6.3: {} shebang-command@2.0.0: dependencies: @@ -2757,10 +2277,6 @@ snapshots: siginfo@2.0.0: {} - signal-exit@3.0.7: {} - - signal-exit@4.1.0: {} - source-map-js@1.2.0: {} source-map@0.6.1: {} @@ -2778,20 +2294,12 @@ snapshots: stoppable@1.1.0: {} - streamsearch@1.1.0: {} - strip-ansi@6.0.1: dependencies: ansi-regex: 5.0.1 - strip-final-newline@3.0.0: {} - strip-json-comments@3.1.1: {} - strip-literal@2.0.0: - dependencies: - js-tokens: 8.0.3 - supports-color@7.2.0: dependencies: has-flag: 4.0.0 @@ -2800,11 +2308,15 @@ snapshots: text-table@0.2.0: {} - tinybench@2.6.0: {} + tinybench@2.9.0: {} - tinypool@0.8.2: {} + tinyexec@0.3.0: {} - tinyspy@2.2.1: {} + tinypool@1.0.1: {} + + tinyrainbow@1.2.0: {} + + tinyspy@3.0.2: {} to-regex-range@5.0.1: dependencies: @@ -2816,53 +2328,32 @@ snapshots: dependencies: prelude-ls: 1.2.1 - type-detect@4.0.8: {} - type-fest@0.20.2: {} typescript@5.3.3: {} - ufo@1.4.0: {} - - ufo@1.5.3: {} - - undici@5.28.2: - dependencies: - '@fastify/busboy': 2.1.1 - - undici@5.28.3: - dependencies: - '@fastify/busboy': 2.1.1 + ufo@1.5.4: {} undici@5.28.4: dependencies: '@fastify/busboy': 2.1.1 - unenv-nightly@1.10.0-1717606461.a117952: + unenv-nightly@2.0.0-20240919-125358-9a64854: dependencies: - consola: 3.2.3 defu: 6.1.4 - mime: 3.0.0 - node-fetch-native: 1.6.4 + ohash: 1.1.4 pathe: 1.1.2 - ufo: 1.5.3 + ufo: 1.5.4 uri-js@4.4.1: dependencies: punycode: 2.3.1 - urlpattern-polyfill@4.0.3: {} - - validate-npm-package-name@4.0.0: - dependencies: - builtins: 5.0.1 - - vite-node@1.3.1(@types/node@18.15.3): + vite-node@2.1.1(@types/node@18.15.3): dependencies: cac: 6.7.14 - debug: 4.3.4 + debug: 4.3.7 pathe: 1.1.2 - picocolors: 1.0.0 vite: 5.2.8(@types/node@18.15.3) transitivePeerDependencies: - '@types/node' @@ -2883,45 +2374,33 @@ snapshots: '@types/node': 18.15.3 fsevents: 2.3.3 - vitest-environment-miniflare@2.14.2(vitest@1.3.1(@types/node@18.15.3)): - dependencies: - '@miniflare/queues': 2.14.2 - '@miniflare/runner-vm': 2.14.2 - '@miniflare/shared': 2.14.2 - '@miniflare/shared-test-environment': 2.14.2 - undici: 5.28.2 - vitest: 1.3.1(@types/node@18.15.3) - transitivePeerDependencies: - - bufferutil - - utf-8-validate - - vitest@1.3.1(@types/node@18.15.3): - dependencies: - '@vitest/expect': 1.3.1 - '@vitest/runner': 1.3.1 - '@vitest/snapshot': 1.3.1 - '@vitest/spy': 1.3.1 - '@vitest/utils': 1.3.1 - acorn-walk: 8.3.2 - chai: 4.4.1 - debug: 4.3.4 - execa: 8.0.1 - local-pkg: 0.5.0 - magic-string: 0.30.7 + vitest@2.1.1(@types/node@18.15.3): + dependencies: + '@vitest/expect': 2.1.1 + '@vitest/mocker': 2.1.1(@vitest/spy@2.1.1)(vite@5.2.8(@types/node@18.15.3)) + '@vitest/pretty-format': 2.1.1 + '@vitest/runner': 2.1.1 + '@vitest/snapshot': 2.1.1 + '@vitest/spy': 2.1.1 + '@vitest/utils': 2.1.1 + chai: 5.1.1 + debug: 4.3.7 + magic-string: 0.30.11 pathe: 1.1.2 - picocolors: 1.0.0 std-env: 3.7.0 - strip-literal: 2.0.0 - tinybench: 2.6.0 - tinypool: 0.8.2 + tinybench: 2.9.0 + tinyexec: 0.3.0 + tinypool: 1.0.1 + tinyrainbow: 1.2.0 vite: 5.2.8(@types/node@18.15.3) - vite-node: 1.3.1(@types/node@18.15.3) - why-is-node-running: 2.2.2 + vite-node: 2.1.1(@types/node@18.15.3) + why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 18.15.3 transitivePeerDependencies: - less - lightningcss + - msw - sass - stylus - sugarss @@ -2932,43 +2411,38 @@ snapshots: dependencies: isexe: 2.0.0 - why-is-node-running@2.2.2: + why-is-node-running@2.3.0: dependencies: siginfo: 2.0.0 stackback: 0.0.2 - workerd@1.20240208.0: + workerd@1.20240909.0: optionalDependencies: - '@cloudflare/workerd-darwin-64': 1.20240208.0 - '@cloudflare/workerd-darwin-arm64': 1.20240208.0 - '@cloudflare/workerd-linux-64': 1.20240208.0 - '@cloudflare/workerd-linux-arm64': 1.20240208.0 - '@cloudflare/workerd-windows-64': 1.20240208.0 + '@cloudflare/workerd-darwin-64': 1.20240909.0 + '@cloudflare/workerd-darwin-arm64': 1.20240909.0 + '@cloudflare/workerd-linux-64': 1.20240909.0 + '@cloudflare/workerd-linux-arm64': 1.20240909.0 + '@cloudflare/workerd-windows-64': 1.20240909.0 - workerd@1.20240610.1: - optionalDependencies: - '@cloudflare/workerd-darwin-64': 1.20240610.1 - '@cloudflare/workerd-darwin-arm64': 1.20240610.1 - '@cloudflare/workerd-linux-64': 1.20240610.1 - '@cloudflare/workerd-linux-arm64': 1.20240610.1 - '@cloudflare/workerd-windows-64': 1.20240610.1 - - wrangler@3.61.0(@cloudflare/workers-types@4.20240614.0): + wrangler@3.78.7(@cloudflare/workers-types@4.20240614.0): dependencies: - '@cloudflare/kv-asset-handler': 0.3.3 + '@cloudflare/kv-asset-handler': 0.3.4 + '@cloudflare/workers-shared': 0.5.3 '@esbuild-plugins/node-globals-polyfill': 0.2.3(esbuild@0.17.19) '@esbuild-plugins/node-modules-polyfill': 0.2.2(esbuild@0.17.19) blake3-wasm: 2.1.5 chokidar: 3.6.0 + date-fns: 3.6.0 esbuild: 0.17.19 - miniflare: 3.20240610.1 + miniflare: 3.20240909.4 nanoid: 3.3.7 - path-to-regexp: 6.2.2 + path-to-regexp: 6.3.0 resolve: 1.22.8 resolve.exports: 2.0.2 selfsigned: 2.4.1 source-map: 0.6.1 - unenv: unenv-nightly@1.10.0-1717606461.a117952 + unenv: unenv-nightly@2.0.0-20240919-125358-9a64854 + workerd: 1.20240909.0 xxhash-wasm: 1.0.2 optionalDependencies: '@cloudflare/workers-types': 4.20240614.0 @@ -2980,18 +2454,12 @@ snapshots: wrappy@1.0.2: {} - ws@8.16.0: {} - ws@8.17.1: {} xxhash-wasm@1.0.2: {} - yallist@4.0.0: {} - yocto-queue@0.1.0: {} - yocto-queue@1.0.0: {} - youch@3.3.3: dependencies: cookie: 0.5.0 diff --git a/src/chunk.ts b/src/chunk.ts index 628d041..f3b2e52 100644 --- a/src/chunk.ts +++ b/src/chunk.ts @@ -60,7 +60,6 @@ export function limit(streamInput: ReadableStream, limitBytes: number): Readable r.releaseLock(); w.releaseLock(); await stream.writable.close(); - await stream.readable.cancel(); })(); return stream.readable; diff --git a/src/errors.ts b/src/errors.ts index c1f8056..95e655f 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -70,6 +70,30 @@ export class InternalError extends Response { } } +export class ManifestError extends Response { + constructor( + code: "MANIFEST_INVALID" | "BLOB_UNKNOWN" | "MANIFEST_UNVERIFIED" | "TAG_INVALID" | "NAME_INVALID", + message: string, + detail: Record = {}, + ) { + const jsonBody = JSON.stringify({ + errors: [ + { + code, + message, + detail, + }, + ], + }); + super(jsonBody, { + status: 400, + headers: { + "content-type": "application/json;charset=UTF-8", + }, + }); + } +} + export class ServerError extends Response { constructor(message: string, errorCode = 500) { super(JSON.stringify({ errors: [{ code: "SERVER_ERROR", message, detail: null }] }), { diff --git a/src/manifest.ts b/src/manifest.ts new file mode 100644 index 0000000..11e2bd1 --- /dev/null +++ b/src/manifest.ts @@ -0,0 +1,31 @@ +import { z } from "zod"; + +// https://github.com/opencontainers/image-spec/blob/main/manifest.md +export const manifestSchema = z.object({ + schemaVersion: z.literal(2), + artifactType: z.string().optional(), + // to maintain retrocompatibility of the registry, let's not assume mediaTypes + mediaType: z.string(), + config: z.object({ + mediaType: z.string(), + digest: z.string(), + size: z.number().int(), + }), + layers: z.array( + z.object({ + size: z.number().int(), + mediaType: z.string(), + digest: z.string(), + }), + ), + annotations: z.record(z.string()).optional(), + subject: z + .object({ + mediaType: z.string(), + digest: z.string(), + size: z.number().int(), + }) + .optional(), +}); + +export type ManifestSchema = z.infer; diff --git a/src/registry/garbage-collector.ts b/src/registry/garbage-collector.ts new file mode 100644 index 0000000..2ab3119 --- /dev/null +++ b/src/registry/garbage-collector.ts @@ -0,0 +1,233 @@ +// We have 2 modes for the garbage collector, unreferenced and untagged. +// Unreferenced will delete all blobs that are not referenced by any manifest. +// Untagged will delete all blobs that are not referenced by any manifest and are not tagged. + +import { ServerError } from "../errors"; +import { ManifestSchema } from "../manifest"; + +export type GarbageCollectionMode = "unreferenced" | "untagged"; +export type GCOptions = { + name: string; + mode: GarbageCollectionMode; +}; + +// The garbage collector checks for dangling layers in the namespace. It's a lock free +// GC, but on-conflict (when there is an ongoing manifest insertion, or an ongoing garbage collection), +// the methods can throw errors. +// +// Summary: +// insertParent() { +// gcMark = getGCMark(); // get last gc mark +// mark = updateInsertMark(); // mark insertion +// defer cleanInsertMark(mark); +// checkEveryChildIsOK(); +// gcMarkIsEqualAndNotOngoingGc(gcMark); // make sure not ongoing deletion mark after checking child is in db +// insertParent(); // insert parent in db +// } +// +// gc() { +// insertionMark = getInsertionMark() // get last insertion mark +// mark = setGCMark() // marks deletion as gc +// defer { cleanGCMark(mark); } // clean up mark +// checkNotOngoingInsertMark(mark) // makes sure not ongoing updateInsertMark, and no new one +// deleteChildrenWithoutParent(); // go ahead and clean children +// } +// +// This makes it so: after every layer is OK we can proceed and insert the manifest, as there is no ongoing GC +// In the GC code, if there is an insertion on-going, there is an error. +export class GarbageCollector { + private registry: R2Bucket; + + constructor(registry: R2Bucket) { + this.registry = registry; + } + + async markForGarbageCollection(namespace: string): Promise { + const etag = crypto.randomUUID(); + const deletion = await this.registry.put(`${namespace}/gc/marker`, etag); + if (deletion === null) throw new Error("unreachable"); + // set last_update so inserters are able to invalidate + await this.registry.put(`${namespace}/gc/last_update`, null, { + customMetadata: { timestamp: `${Date.now()}-${crypto.randomUUID()}` }, + }); + return etag; + } + + async cleanupGarbageCollectionMark(namespace: string) { + // set last_update so inserters can confirm that a GC didnt happen while they were confirming data + await this.registry.put(`${namespace}/gc/last_update`, null, { + customMetadata: { timestamp: `${Date.now()}-${crypto.randomUUID()}` }, + }); + await this.registry.delete(`${namespace}/gc/marker`); + } + + async getGCMarker(namespace: string): Promise { + const object = await this.registry.head(`${namespace}/gc/last_update`); + if (object === null) { + return ""; + } + + if (object.customMetadata === undefined) { + return ""; + } + + return object.customMetadata["timestamp"] ?? "mark"; + } + + async checkCanInsertData(namespace: string, mark: string): Promise { + const gcMarker = await this.registry.head(`${namespace}/gc/marker`); + if (gcMarker !== null) { + return false; + } + + const newMarker = await this.getGCMarker(namespace); + // There's been a new garbage collection since we started the check for insertion + if (newMarker !== mark) return false; + + return true; + } + + // If successful, it inserted in R2 that its going + // to start inserting data that might conflight with GC. + async markForInsertion(namespace: string): Promise { + const uid = crypto.randomUUID(); + // mark that there is an on-going insertion + const deletion = await this.registry.put(`${namespace}/insertion/${uid}`, uid); + if (deletion === null) throw new Error("unreachable"); + // set last_update so GC is able to invalidate + await this.registry.put(`${namespace}/insertion/last_update`, null, { + customMetadata: { timestamp: `${Date.now()}-${crypto.randomUUID()}` }, + }); + + return uid; + } + + async cleanInsertion(namespace: string, tag: string) { + // update again to invalidate GC and the insertion is safe + await this.registry.put(`${namespace}/insertion/last_update`, null, { + customMetadata: { timestamp: `${Date.now()}-${crypto.randomUUID()}` }, + }); + + await this.registry.delete(`${namespace}/insertion/${tag}`); + } + + async getInsertionMark(namespace: string): Promise { + const object = await this.registry.head(`${namespace}/insertion/last_update`); + if (object === null) { + return ""; + } + + if (object.customMetadata === undefined) { + return ""; + } + + return object.customMetadata["timestamp"] ?? "mark"; + } + + async checkIfGCCanContinue(namespace: string, mark: string): Promise { + const objects = await this.registry.list({ prefix: `${namespace}/insertion` }); + for (const object of objects.objects) { + if (object.key.endsWith("/last_update")) continue; + if (object.uploaded.getTime() + 1000 * 60 <= Date.now()) { + await this.registry.delete(object.key); + } else { + return false; + } + } + + // call again to clean more + if (objects.truncated) return false; + + const newMark = await this.getInsertionMark(namespace); + if (newMark !== mark) { + return false; + } + + return true; + } + + private async list(prefix: string, callback: (object: R2Object) => Promise): Promise { + const listed = await this.registry.list({ prefix }); + for (const object of listed.objects) { + if ((await callback(object)) === false) { + return false; + } + } + + let truncated = listed.truncated; + let cursor = listed.truncated ? listed.cursor : undefined; + + while (truncated) { + const next = await this.registry.list({ prefix, cursor }); + for (const object of next.objects) { + if ((await callback(object)) === false) { + return false; + } + } + truncated = next.truncated; + cursor = truncated ? cursor : undefined; + } + return true; + } + + async collect(options: GCOptions): Promise { + await this.markForGarbageCollection(options.name); + try { + return await this.collectInner(options); + } finally { + // if this fails, user can always call a custom endpoint to clean it up + await this.cleanupGarbageCollectionMark(options.name); + } + } + + private async collectInner(options: GCOptions): Promise { + // We can run out of memory, this should be a bloom filter + let referencedBlobs = new Set(); + const mark = await this.getInsertionMark(options.name); + + await this.list(`${options.name}/manifests/`, async (manifestObject) => { + const tag = manifestObject.key.split("/").pop(); + if (!tag || (options.mode === "untagged" && tag.startsWith("sha256:"))) { + return true; + } + const manifest = await this.registry.get(manifestObject.key); + if (!manifest) { + return true; + } + + const manifestData = (await manifest.json()) as ManifestSchema; + manifestData.layers.forEach((layer) => { + referencedBlobs.add(layer.digest); + }); + + return true; + }); + + let unreferencedKeys: string[] = []; + const deleteThreshold = 15; + await this.list(`${options.name}/blobs/`, async (object) => { + const hash = object.key.split("/").pop(); + if (hash && !referencedBlobs.has(hash)) { + unreferencedKeys.push(object.key); + if (unreferencedKeys.length > deleteThreshold) { + if (!(await this.checkIfGCCanContinue(options.name, mark))) { + throw new ServerError("there is a manifest insertion going, the garbage collection shall stop"); + } + + await this.registry.delete(unreferencedKeys); + unreferencedKeys = []; + } + } + return true; + }); + if (unreferencedKeys.length > 0) { + if (!(await this.checkIfGCCanContinue(options.name, mark))) { + throw new Error("there is a manifest insertion going, the garbage collection shall stop"); + } + + await this.registry.delete(unreferencedKeys); + } + + return true; + } +} diff --git a/src/registry/http.ts b/src/registry/http.ts index 9ca1b24..ae3734d 100644 --- a/src/registry/http.ts +++ b/src/registry/http.ts @@ -1,6 +1,7 @@ import { Env } from "../.."; import { InternalError } from "../errors"; import { errorString } from "../utils"; +import { GarbageCollectionMode } from "./garbage-collector"; import { CheckLayerResponse, CheckManifestResponse, @@ -473,6 +474,10 @@ export class RegistryHTTPClient implements Registry { async listRepositories(_limit?: number, _last?: string): Promise { throw new Error("unimplemented"); } + + garbageCollection(_namespace: string, _mode: GarbageCollectionMode): Promise { + throw new Error("unimplemented"); + } } // AuthType defined the supported auth types diff --git a/src/registry/r2.ts b/src/registry/r2.ts index 14b0e06..5a6bc8f 100644 --- a/src/registry/r2.ts +++ b/src/registry/r2.ts @@ -9,7 +9,7 @@ import { limit, split, } from "../chunk"; -import { InternalError, RangeError, ServerError } from "../errors"; +import { InternalError, ManifestError, RangeError, ServerError } from "../errors"; import { SHA256_PREFIX_LEN, getSHA256, hexToDigest } from "../user"; import { readableToBlob, readerToBlob, wrap } from "../utils"; import { BlobUnknownError, ManifestUnknownError } from "../v2-errors"; @@ -27,6 +27,8 @@ import { UploadObject, wrapError, } from "./registry"; +import { GarbageCollectionMode, GarbageCollector } from "./garbage-collector"; +import { ManifestSchema, manifestSchema } from "../manifest"; export type Chunk = | { @@ -126,7 +128,11 @@ export async function getUploadState( } export class R2Registry implements Registry { - constructor(private env: Env) {} + private gc: GarbageCollector; + + constructor(private env: Env) { + this.gc = new GarbageCollector(this.env.REGISTRY); + } async manifestExists(name: string, reference: string): Promise { const [res, err] = await wrap(this.env.REGISTRY.head(`${name}/manifests/${reference}`)); @@ -169,13 +175,19 @@ export class R2Registry implements Registry { const repositories: Record = {}; let totalRecords = 0; let lastSeen: string | undefined; - const objectExistsInPath = (entry: string) => { + const objectExistsInPath = (entry?: string) => { + if (entry === undefined) return false; const parts = entry.split("/"); const repository = parts.slice(0, parts.length - 2).join("/"); return repository in repositories; }; + const repositoriesOrder: string[] = []; const addObjectPath = (object: R2Object) => { + if (totalRecords >= options.limit && !objectExistsInPath(object.key)) { + return; + } + // update lastSeen for cursoring purposes lastSeen = object.key; // don't add if seen before @@ -185,22 +197,23 @@ export class R2Registry implements Registry { // /<'blobs' | 'manifests'>/ const parts = object.key.split("/"); const repository = parts.slice(0, parts.length - 2).join("/"); - if (!(repository in repositories)) { - totalRecords++; - } + if (parts[parts.length - 2] === "blobs") return; + if (repository in repositories) return; + totalRecords++; repositories[repository] = {}; + repositoriesOrder.push(repository); }; const r2Objects = await this.env.REGISTRY.list({ - limit: options.limit, + limit: 50, startAfter: options.startAfter, }); r2Objects.objects.forEach((path) => addObjectPath(path)); let cursor = r2Objects.truncated ? r2Objects.cursor : undefined; while (cursor !== undefined && totalRecords < options.limit) { const next = await this.env.REGISTRY.list({ - limit: options.limit, + limit: 50, cursor, }); next.objects.forEach((path) => addObjectPath(path)); @@ -213,18 +226,19 @@ export class R2Registry implements Registry { while (cursor !== undefined && typeof lastSeen === "string" && objectExistsInPath(lastSeen)) { const nextList: R2Objects = await this.env.REGISTRY.list({ - limit: 1000, + limit: 50, cursor, }); let found = false; // Search for the next object in the list for (const object of nextList.objects) { - lastSeen = object.key; if (!objectExistsInPath(lastSeen)) { found = true; break; } + + lastSeen = object.key; } if (found) break; @@ -239,22 +253,47 @@ export class R2Registry implements Registry { } } - if (cursor === undefined) { - lastSeen = undefined; - } - return { - repositories: Object.keys(repositories), + repositories: repositoriesOrder, cursor: lastSeen, }; } + async verifyManifest(name: string, manifest: ManifestSchema) { + const layers = [...manifest.layers, manifest.config]; + for (const key of layers) { + const res = await this.env.REGISTRY.head(`${name}/blobs/${key.digest}`); + if (res === null) { + console.error(`Digest ${key} doesn't exist`); + return new ManifestError("BLOB_UNKNOWN", `unknown blob ${key.digest}`); + } + } + + return null; + } + async putManifest( + namespace: string, + reference: string, + readableStream: ReadableStream, + contentType: string, + ): Promise { + const key = await this.gc.markForInsertion(namespace); + try { + return this.putManifestInner(namespace, reference, readableStream, contentType); + } finally { + // if this fails, at some point it will be expired + await this.gc.cleanInsertion(namespace, key); + } + } + + async putManifestInner( name: string, reference: string, readableStream: ReadableStream, contentType: string, ): Promise { + const gcMarker = await this.gc.getGCMarker(name); const env = this.env; const sha256 = new crypto.DigestStream("SHA-256"); const reader = readableStream.getReader(); @@ -265,10 +304,19 @@ export class R2Registry implements Registry { const digest = await sha256.digest; const digestStr = hexToDigest(digest); const text = await blob.text(); + const manifestJSON = JSON.parse(text); + const manifest = manifestSchema.parse(manifestJSON); + const verifyManifestErr = await this.verifyManifest(name, manifest); + if (verifyManifestErr !== null) return { response: verifyManifestErr }; + + if (!(await this.gc.checkCanInsertData(name, gcMarker))) { + console.error("Manifest can't be uploaded as there is/was a garbage collection going"); + return { response: new ServerError("garbage collection is on-going... check with registry administrator", 500) }; + } + const putReference = async () => { // if the reference is the same as a digest, it's not necessary to insert if (reference === digestStr) return; - // TODO: If we're overriding an existing manifest here, should we update the original manifest references? return await env.REGISTRY.put(`${name}/manifests/${reference}`, text, { sha256: digest, httpMetadata: { @@ -276,7 +324,8 @@ export class R2Registry implements Registry { }, }); }; - await Promise.allSettled([ + + await Promise.all([ putReference(), // this is the "main" manifest env.REGISTRY.put(`${name}/manifests/${digestStr}`, text, { @@ -681,4 +730,9 @@ export class R2Registry implements Registry { location: `/v2/${namespace}/blobs/${sha256}`, }; } + + async garbageCollection(namespace: string, mode: GarbageCollectionMode): Promise { + const result = await this.gc.collect({ name: namespace, mode: mode }); + return result; + } } diff --git a/src/registry/registry.ts b/src/registry/registry.ts index cffbf60..8c684b2 100644 --- a/src/registry/registry.ts +++ b/src/registry/registry.ts @@ -2,6 +2,7 @@ import { Env } from "../.."; import { InternalError } from "../errors"; import { errorString } from "../utils"; import z from "zod"; +import { GarbageCollectionMode } from "./garbage-collector"; // Defines a registry and how it's configured const registryConfiguration = z.object({ @@ -166,6 +167,8 @@ export interface Registry { stream?: ReadableStream, length?: number, ): Promise; + + garbageCollection(namespace: string, mode: GarbageCollectionMode): Promise; } export function wrapError(method: string, err: unknown): RegistryError { diff --git a/src/router.ts b/src/router.ts index 0728db8..0a92340 100644 --- a/src/router.ts +++ b/src/router.ts @@ -562,4 +562,14 @@ v2Router.delete("/:name+/blobs/:digest", async (req, env: Env) => { }); }); +v2Router.post("/:name+/gc", async (req, env: Env) => { + const { name } = req.params; + const mode = req.query.mode ?? "unreferenced"; + if (mode !== "unreferenced" && mode !== "untagged") { + throw new ServerError("Mode must be either 'unreferenced' or 'untagged'", 400); + } + const result = await env.REGISTRY_CLIENT.garbageCollection(name, mode); + return new Response(JSON.stringify({ success: result })); +}); + export default v2Router; diff --git a/index.test.ts b/test/index.test.ts similarity index 68% rename from index.test.ts rename to test/index.test.ts index 49c3b74..41f90b3 100644 --- a/index.test.ts +++ b/test/index.test.ts @@ -1,13 +1,35 @@ -import { afterAll, beforeEach, describe, expect, test } from "vitest"; -import { SHA256_PREFIX_LEN, getSHA256 } from "./src/user"; -import v2Router, { TagsList } from "./src/router"; -import { Env } from "."; -import * as fetchAuth from "./index"; -import { RegistryTokens } from "./src/token"; -import { RegistryAuthProtocolTokenPayload } from "./src/auth"; -import { registries } from "./src/registry/registry"; -import { RegistryHTTPClient } from "./src/registry/http"; +import { afterAll, describe, expect, test } from "vitest"; +import { SHA256_PREFIX_LEN, getSHA256 } from "../src/user"; +import { TagsList } from "../src/router"; +import { Env } from ".."; +import { RegistryTokens } from "../src/token"; +import { RegistryAuthProtocolTokenPayload } from "../src/auth"; +import { registries } from "../src/registry/registry"; +import { RegistryHTTPClient } from "../src/registry/http"; import { encode } from "@cfworker/base64url"; +import { ManifestSchema } from "../src/manifest"; +import { limit } from "../src/chunk"; +import worker from "../index"; +import { createExecutionContext, env, waitOnExecutionContext } from "cloudflare:test"; + +async function generateManifest(name: string): Promise { + const data = "bla"; + const sha256 = await getSHA256(data); + const res = await fetch(createRequest("POST", `/v2/${name}/blobs/uploads/`, null, {})); + expect(res.ok).toBeTruthy(); + const blob = new Blob([data]).stream(); + const stream = limit(blob, data.length); + const res2 = await fetch(createRequest("PATCH", res.headers.get("location")!, stream, {})); + expect(res2.ok).toBeTruthy(); + const last = await fetch(createRequest("PUT", res2.headers.get("location")! + "&digest=" + sha256, null, {})); + expect(last.ok).toBeTruthy(); + return { + schemaVersion: 2, + layers: [{ size: data.length, digest: sha256, mediaType: "shouldbeanything" }], + config: { size: data.length, digest: sha256, mediaType: "configmediatypeshouldntbechecked" }, + mediaType: "shouldalsobeanythingforretrocompatibility", + }; +} function createRequest(method: string, path: string, body: ReadableStream | null, headers = {}) { return new Request(new URL("https://registry.com" + path), { method, body: body, headers }); @@ -26,27 +48,26 @@ function usernamePasswordToAuth(username: string, password: string): string { return `Basic ${btoa(`${username}:${password}`)}`; } -const bindings = getMiniflareBindings() as Env; async function fetchUnauth(r: Request): Promise { - const res = await v2Router.handle(r, bindings); + const ctx = createExecutionContext(); + const res = await worker.fetch(r, env as Env, ctx); + await waitOnExecutionContext(ctx); return res as Response; } async function fetch(r: Request): Promise { - return fetchAuth.default.fetch(r, bindings); + r.headers.append("Authorization", usernamePasswordToAuth("hello", "world")); + return await fetchUnauth(r); } describe("v2", () => { test("/v2", async () => { - const response = await fetchUnauth(createRequest("GET", "/v2/", null)); + const response = await fetch(createRequest("GET", "/v2/", null)); expect(response.status).toBe(200); }); test("Username password authenticatiom fails gracefully when wrong format", async () => { - const bindings = getMiniflareBindings() as Env; - bindings.USERNAME = "hello"; - bindings.PASSWORD = "world"; - const res = await fetch( + const res = await fetchUnauth( createRequest("GET", `/v2/`, null, { Authorization: `Basic ${encode("hello")}:${encode("t")}`, }), @@ -55,11 +76,8 @@ describe("v2", () => { }); test("Username password authenticatiom fails gracefully when password is wrong", async () => { - const bindings = getMiniflareBindings() as Env; - bindings.USERNAME = "hello"; - bindings.PASSWORD = "world"; const cred = encode(`hello:t`); - const res = await fetch( + const res = await fetchUnauth( createRequest("GET", `/v2/`, null, { Authorization: `Basic ${cred}`, }), @@ -68,11 +86,8 @@ describe("v2", () => { }); test("Simple username password authenticatiom fails gracefully when password is wrong", async () => { - const bindings = getMiniflareBindings() as Env; - bindings.USERNAME = "hello"; - bindings.PASSWORD = "world"; const cred = encode(`hello:t`); - const res = await fetch( + const res = await fetchUnauth( createRequest("GET", `/v2/`, null, { Authorization: `Basic ${cred}`, }), @@ -81,11 +96,8 @@ describe("v2", () => { }); test("Simple username password authenticatiom fails gracefully when username is wrong", async () => { - const bindings = getMiniflareBindings() as Env; - bindings.USERNAME = "hello"; - bindings.PASSWORD = "world"; const cred = encode(`hell0:world`); - const res = await fetch( + const res = await fetchUnauth( createRequest("GET", `/v2/`, null, { Authorization: `Basic ${cred}`, }), @@ -94,19 +106,17 @@ describe("v2", () => { }); test("Simple username password authentication", async () => { - const bindings = getMiniflareBindings() as Env; - bindings.USERNAME = "hello"; - bindings.PASSWORD = "world"; - const res = await fetch(createRequest("GET", `/v2/`, null, {})); + const res = await fetchUnauth(createRequest("GET", `/v2/`, null, {})); expect(res.status).toBe(401); expect(res.ok).toBeFalsy(); - const resAuth = await fetch( + const resAuth = await fetchUnauth( createRequest("GET", `/v2/`, null, { Authorization: usernamePasswordToAuth("hellO", "worlD"), }), ); + expect(resAuth.status).toBe(401); expect(resAuth.ok).toBeFalsy(); - const resAuthCorrect = await fetch( + const resAuthCorrect = await fetchUnauth( createRequest("GET", `/v2/`, null, { Authorization: usernamePasswordToAuth("hello", "world"), }), @@ -115,17 +125,21 @@ describe("v2", () => { }); }); -async function createManifest(name: string, data: string, tag?: string): Promise<{ sha256: string }> { +async function createManifest(name: string, schema: ManifestSchema, tag?: string): Promise<{ sha256: string }> { + const data = JSON.stringify(schema); const sha256 = await getSHA256(data); if (!tag) { tag = sha256; } - const res = await fetchUnauth( + const res = await fetch( createRequest("PUT", `/v2/${name}/manifests/${tag}`, new Blob([data]).stream(), { "Content-Type": "application/gzip", }), ); + if (!res.ok) { + throw new Error(await res.text()); + } expect(res.ok).toBeTruthy(); expect(res.headers.get("docker-content-digest")).toEqual(sha256); return { sha256 }; @@ -133,7 +147,7 @@ async function createManifest(name: string, data: string, tag?: string): Promise describe("v2 manifests", () => { test("HEAD /v2/:name/manifests/:reference NOT FOUND", async () => { - const response = await fetchUnauth(createRequest("GET", "/v2/notfound/manifests/reference", null)); + const response = await fetch(createRequest("GET", "/v2/notfound/manifests/reference", null)); expect(response.status).toBe(404); const json = await response.json(); expect(json).toEqual({ @@ -154,11 +168,12 @@ describe("v2 manifests", () => { const name = "name"; const data = "{}"; const sha256 = await getSHA256(data); + const bindings = env as Env; await bindings.REGISTRY.put(`${name}/manifests/${reference}`, "{}", { httpMetadata: { contentType: "application/gzip" }, sha256: sha256.slice(SHA256_PREFIX_LEN), }); - const res = await fetchUnauth(createRequest("HEAD", `/v2/${name}/manifests/${reference}`, null)); + const res = await fetch(createRequest("HEAD", `/v2/${name}/manifests/${reference}`, null)); expect(res.ok).toBeTruthy(); expect(Object.fromEntries(res.headers)).toEqual({ "content-length": "2", @@ -168,47 +183,75 @@ describe("v2 manifests", () => { await bindings.REGISTRY.delete(`${name}/manifests/${reference}`); }); - test("PUT /v2/:name/manifests/:reference works", () => createManifest("hello-world-main", "{}", "hello")); - test("PUT then DELETE /v2/:name/manifests/:reference works", async () => { - const { sha256 } = await createManifest("hello-world", "{}", "hello"); + const { sha256 } = await createManifest("hello-world", await generateManifest("hello-world"), "hello"); + const bindings = env as Env; + + { + const listObjects = await bindings.REGISTRY.list({ prefix: "hello-world/blobs/" }); + expect(listObjects.objects.length).toEqual(1); + + const gcRes = await fetch(new Request("http://registry.com/v2/hello-world/gc", { method: "POST" })); + if (!gcRes.ok) { + throw new Error(`${gcRes.status}: ${await gcRes.text()}`); + } + + const listObjectsAfterGC = await bindings.REGISTRY.list({ prefix: "hello-world/blobs/" }); + expect(listObjectsAfterGC.objects.length).toEqual(1); + } + expect(await bindings.REGISTRY.head(`hello-world/manifests/hello`)).toBeTruthy(); - const res = await fetchUnauth(createRequest("DELETE", `/v2/hello-world/manifests/${sha256}`, null)); + const res = await fetch(createRequest("DELETE", `/v2/hello-world/manifests/${sha256}`, null)); expect(res.status).toEqual(202); expect(await bindings.REGISTRY.head(`hello-world/manifests/${sha256}`)).toBeNull(); expect(await bindings.REGISTRY.head(`hello-world/manifests/hello`)).toBeNull(); + + const listObjects = await bindings.REGISTRY.list({ prefix: "hello-world/blobs/" }); + expect(listObjects.objects.length).toEqual(1); + + const listObjectsManifests = await bindings.REGISTRY.list({ prefix: "hello-world/manifests/" }); + expect(listObjectsManifests.objects.length).toEqual(0); + + const gcRes = await fetch(new Request("http://registry.com/v2/hello-world/gc", { method: "POST" })); + if (!gcRes.ok) { + throw new Error(`${gcRes.status}: ${await gcRes.text()}`); + } + + const listObjectsAfterGC = await bindings.REGISTRY.list({ prefix: "hello-world/blobs/" }); + expect(listObjectsAfterGC.objects.length).toEqual(0); }); test("PUT multiple parts then DELETE /v2/:name/manifests/:reference works", async () => { - const { sha256 } = await createManifest("hello/world", "{}", "hello"); + const { sha256 } = await createManifest("hello/world", await generateManifest("hello/world"), "hello"); + const bindings = env as Env; expect(await bindings.REGISTRY.head(`hello/world/manifests/hello`)).toBeTruthy(); - const res = await fetchUnauth(createRequest("DELETE", `/v2/hello/world/manifests/${sha256}`, null)); + const res = await fetch(createRequest("DELETE", `/v2/hello/world/manifests/${sha256}`, null)); expect(res.status).toEqual(202); expect(await bindings.REGISTRY.head(`hello/world/manifests/${sha256}`)).toBeNull(); expect(await bindings.REGISTRY.head(`hello/world/manifests/hello`)).toBeNull(); }); test("PUT then list tags with GET /v2/:name/tags/list", async () => { - const { sha256 } = await createManifest("hello-world-list", "{}", `hello`); + const { sha256 } = await createManifest("hello-world-list", await generateManifest("hello-world-list"), `hello`); const expectedRes = ["hello", sha256]; - for (let i = 0; i < 500; i++) { + for (let i = 0; i < 50; i++) { expectedRes.push(`hello-${i}`); } expectedRes.sort(); const shuffledRes = shuffleArray([...expectedRes]); for (const tag of shuffledRes) { - await createManifest("hello-world-list", "{}", tag); + await createManifest("hello-world-list", await generateManifest("hello-world-list"), tag); } - const tagsRes = await fetchUnauth(createRequest("GET", `/v2/hello-world-list/tags/list?n=1000`, null)); + const tagsRes = await fetch(createRequest("GET", `/v2/hello-world-list/tags/list?n=1000`, null)); const tags = (await tagsRes.json()) as TagsList; expect(tags.name).toEqual("hello-world-list"); expect(tags.tags).toEqual(expectedRes); - const res = await fetchUnauth(createRequest("DELETE", `/v2/hello-world-list/manifests/${sha256}`, null)); + const res = await fetch(createRequest("DELETE", `/v2/hello-world-list/manifests/${sha256}`, null)); expect(res.ok).toBeTruthy(); - const tagsResEmpty = await fetchUnauth(createRequest("GET", `/v2/hello-world-list/tags/list`, null)); + const tagsResEmpty = await fetch(createRequest("GET", `/v2/hello-world-list/tags/list`, null)); const tagsEmpty = (await tagsResEmpty.json()) as TagsList; expect(tagsEmpty.tags).toHaveLength(0); }); @@ -369,6 +412,7 @@ test("registries configuration", async () => { }, ] as const; + const bindings = env as Env; const bindingCopy = { ...bindings }; for (const testCase of testCases) { bindingCopy.REGISTRIES_JSON = testCase.configuration; @@ -392,14 +436,9 @@ test("registries configuration", async () => { }); describe("http client", () => { + const bindings = env as Env; let envBindings = { ...bindings }; const prevFetch = global.fetch; - beforeEach(() => { - global.fetch = async function (info, init) { - const r = new Request(info, init); - return fetchAuth.default.fetch(r, envBindings); - }; - }); afterAll(() => { global.fetch = prevFetch; @@ -408,13 +447,16 @@ describe("http client", () => { test("test manifest exists", async () => { envBindings = { ...bindings }; envBindings.JWT_REGISTRY_TOKENS_PUBLIC_KEY = ""; - envBindings.PASSWORD = "123456"; - envBindings.USERNAME = "v1"; + envBindings.PASSWORD = "world"; + envBindings.USERNAME = "hello"; envBindings.REGISTRIES_JSON = undefined; + global.fetch = async function (r: RequestInfo): Promise { + return fetch(new Request(r)); + }; const client = new RegistryHTTPClient(envBindings, { registry: "https://localhost", password_env: "PASSWORD", - username: "v1", + username: "hello", }); const res = await client.manifestExists("namespace/hello", "latest"); if ("response" in res) { @@ -427,33 +469,33 @@ describe("http client", () => { describe("push and catalog", () => { test("push and then use the catalog", async () => { - await createManifest("hello-world-main", "{}", "hello"); - await createManifest("hello-world-main", "{}", "latest"); - await createManifest("hello-world-main", "{}", "hello-2"); - await createManifest("hello", "{}", "hello"); - await createManifest("hello/hello", "{}", "hello"); + await createManifest("hello-world-main", await generateManifest("hello-world-main"), "hello"); + await createManifest("hello-world-main", await generateManifest("hello-world-main"), "latest"); + await createManifest("hello-world-main", await generateManifest("hello-world-main"), "hello-2"); + await createManifest("hello", await generateManifest("hello"), "hello"); + await createManifest("hello/hello", await generateManifest("hello/hello"), "hello"); - const response = await fetchUnauth(createRequest("GET", "/v2/_catalog", null)); + const response = await fetch(createRequest("GET", "/v2/_catalog", null)); expect(response.ok).toBeTruthy(); const body = (await response.json()) as { repositories: string[] }; expect(body).toEqual({ repositories: ["hello-world-main", "hello/hello", "hello"], }); const expectedRepositories = body.repositories; - const tagsRes = await fetchUnauth(createRequest("GET", `/v2/hello-world-main/tags/list?n=1000`, null)); + const tagsRes = await fetch(createRequest("GET", `/v2/hello-world-main/tags/list?n=1000`, null)); const tags = (await tagsRes.json()) as TagsList; expect(tags.name).toEqual("hello-world-main"); expect(tags.tags).toEqual([ "hello", "hello-2", "latest", - "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a", + "sha256:e8f14a06f5e206931feb6761a6022231c98917edeb9d7f44c88f075113656374", ]); const repositoryBuildUp: string[] = []; let currentPath = "/v2/_catalog?n=1"; for (let i = 0; i < 3; i++) { - const response = await fetchUnauth(createRequest("GET", currentPath, null)); + const response = await fetch(createRequest("GET", currentPath, null)); expect(response.ok).toBeTruthy(); const body = (await response.json()) as { repositories: string[] }; if (body.repositories.length === 0) { diff --git a/test/tsconfig.json b/test/tsconfig.json new file mode 100644 index 0000000..1ea79c3 --- /dev/null +++ b/test/tsconfig.json @@ -0,0 +1,13 @@ +{ + "include": ["vitest.config.ts"], + "compilerOptions": { + "strict": true, + "module": "esnext", + "target": "esnext", + "lib": ["esnext"], + "moduleResolution": "bundler", + "noEmit": true, + "skipLibCheck": true, + "allowSyntheticDefaultImports": true + } +} diff --git a/test/vitest.config.ts b/test/vitest.config.ts new file mode 100644 index 0000000..e8fefd3 --- /dev/null +++ b/test/vitest.config.ts @@ -0,0 +1,12 @@ +import { defineWorkersProject } from "@cloudflare/vitest-pool-workers/config"; + +export default defineWorkersProject({ + test: { + poolOptions: { + workers: { + singleWorker: true, + wrangler: { configPath: "./wrangler.test.toml", environment: "dev" }, + }, + }, + }, +}); diff --git a/test/wrangler.test.toml b/test/wrangler.test.toml new file mode 100644 index 0000000..20d8ad8 --- /dev/null +++ b/test/wrangler.test.toml @@ -0,0 +1,21 @@ +name = "r2-registry" + +workers_dev = true +main = "../index.ts" +compatibility_date = "2024-09-09" +compatibility_flags = ["nodejs_compat"] + +## Production +[env.production] +r2_buckets = [ + { binding = "REGISTRY", bucket_name = "" } +] + +[env.production.vars] +JWT_REGISTRY_TOKENS_PUBLIC_KEY = "" +[env.dev] +r2_buckets = [{ binding = "REGISTRY", bucket_name = "r2-image-registry-dev" }] + +[env.dev.vars] +USERNAME = "hello" +PASSWORD = "world" diff --git a/tsconfig.base.json b/tsconfig.base.json index 65f0521..93533ae 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -5,7 +5,7 @@ "experimentalDecorators": true, "module": "esnext", "moduleResolution": "node", - "types": ["@cloudflare/workers-types", "vitest-environment-miniflare/globals"], + "types": ["@cloudflare/workers-types", "@cloudflare/vitest-pool-workers"], "resolveJsonModule": true, "allowJs": true, "noEmit": true, diff --git a/tsconfig.json b/tsconfig.json index d9b6b99..b6e3127 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,5 +1,5 @@ { "extends": "./tsconfig.base.json", - "include": ["src/**/*.ts", "*.ts"], - "exclude": ["src/**/*.js", "dist/**/*.ts"] + "include": ["src/**/*.ts", "index.ts", "test/index.test.ts"], + "exclude": ["src/**/*.js", "dist/**/*.ts", "test/vitest.config.ts"] } diff --git a/vitest.config.ts b/vitest.config.ts deleted file mode 100644 index 40440a0..0000000 --- a/vitest.config.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { defineConfig } from "vitest/config"; - -export default defineConfig({ - test: { - environment: "miniflare", - // Configuration is automatically loaded from `.env`, `package.json` and - // `wrangler.toml` files by default, but you can pass any additional Miniflare - // API options here: - environmentOptions: { - r2Buckets: ["REGISTRY"], - }, - }, -}); diff --git a/wrangler.toml.example b/wrangler.toml.example index 36c5c5a..1f00fda 100644 --- a/wrangler.toml.example +++ b/wrangler.toml.example @@ -2,8 +2,8 @@ name = "r2-registry" workers_dev = true main = "./index.ts" -compatibility_date = "2022-04-18" -compatibility_flags = ["streams_enable_constructors"] +compatibility_date = "2024-09-09" +compatibility_flags = ["nodejs_compat"] ## Production [env.production]