├── .env.local ├── .github └── workflows │ └── main.yml ├── .gitignore ├── .vscode ├── launch.json └── settings.json ├── Dockerfile ├── README.md ├── deno.json ├── deno.lock ├── docker-compose.yml ├── e2e └── e2e.test.ts ├── scripts └── wait-for-bucket.ts └── src ├── index.test.ts └── index.ts /.env.local: -------------------------------------------------------------------------------- 1 | NX_CACHE_ACCESS_TOKEN=test-token 2 | 3 | AWS_REGION=us-east-1 4 | AWS_ACCESS_KEY_ID=minio 5 | AWS_SECRET_ACCESS_KEY=minio123 6 | S3_BUCKET_NAME=nx-cloud 7 | S3_ENDPOINT_URL=http://localhost:9000 8 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Lint, Test and Build 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - 'main' 8 | release: 9 | types: [published] 10 | pull_request: 11 | branches: 12 | - 'main' 13 | 14 | jobs: 15 | checks: 16 | name: Main Job 17 | runs-on: ubuntu-latest 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@v4 21 | - name: Setup Deno 22 | uses: denoland/setup-deno@v2 23 | with: 24 | deno-version: v2.3.3 25 | - name: Run lint 26 | run: deno lint 27 | - name: Run format check 28 | run: deno fmt --check 29 | e2e: 30 | name: Test Job 31 | runs-on: ubuntu-latest 32 | steps: 33 | - name: Checkout 34 | uses: actions/checkout@v4 35 | - name: Setup Deno 36 | uses: denoland/setup-deno@v2 37 | with: 38 | deno-version: v2.3.3 39 | - name: Run MinIO 40 | run: | 41 | docker compose -f docker-compose.yml up -d 42 | deno --no-lock -A npm:wait-on http://localhost:3000/health 43 | - name: Run tests 44 | run: | 45 | deno task test 46 | deno task e2e 47 | - name: Run MinIO down 48 | run: | 49 | docker compose down 50 | publish: 51 | runs-on: ubuntu-latest 52 | needs: 53 | - e2e 54 | - checks 55 | env: 56 | REGISTRY: ghcr.io 57 | IMAGE_NAME: ikatsuba/nx-cache-server 58 | permissions: 59 | contents: read 60 | packages: write 61 | steps: 62 | - uses: actions/checkout@v4 63 | - name: Docker meta 64 | id: meta 65 | uses: docker/metadata-action@v5 66 | with: 67 | images: | 68 | ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} 69 | tags: | 70 | type=ref,event=branch 71 | type=ref,event=pr 72 | type=semver,pattern={{version}} 73 | type=semver,pattern={{major}}.{{minor}} 74 | type=semver,pattern={{major}} 75 | - name: Set up Docker Buildx 76 | uses: docker/setup-buildx-action@v3 77 | - name: Login to GitHub Container Registry 78 | uses: docker/login-action@v3 79 | with: 80 | registry: ${{ env.REGISTRY }} 81 | username: ${{ github.actor }} 82 | password: ${{ secrets.GITHUB_TOKEN }} 83 | - name: Build and push 84 | uses: docker/build-push-action@v6 85 | with: 86 | context: . 87 | file: ./Dockerfile 88 | push: true 89 | tags: ${{ steps.meta.outputs.tags }} 90 | labels: ${{ steps.meta.outputs.labels }} 91 | platforms: | 92 | linux/amd64 93 | linux/arm64/v8 94 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | 3 | tmp -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "request": "launch", 9 | "name": "Serve", 10 | "type": "node", 11 | "program": "${workspaceFolder}/src/index.ts", 12 | "cwd": "${workspaceFolder}", 13 | "env": {}, 14 | "runtimeExecutable": "/Users/igorkatsuba/.deno/bin/deno", 15 | "runtimeArgs": [ 16 | "run", 17 | "--inspect-wait", 18 | "--allow-all", 19 | "--env-file=.env.local" 20 | ], 21 | "attachSimplePort": 9229 22 | }, 23 | { 24 | "request": "launch", 25 | "name": "Test", 26 | "type": "node", 27 | "program": "${workspaceFolder}/src/index.test.ts", 28 | "cwd": "${workspaceFolder}", 29 | "env": {}, 30 | "runtimeExecutable": "/Users/igorkatsuba/.deno/bin/deno", 31 | "runtimeArgs": [ 32 | "test", 33 | "--inspect-wait", 34 | "--allow-all", 35 | "--env-file=.env.local" 36 | ], 37 | "attachSimplePort": 9229 38 | } 39 | ] 40 | } 41 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "deno.enable": true, 3 | "deno.lint": true, 4 | "editor.formatOnSave": true, 5 | "editor.defaultFormatter": "denoland.vscode-deno", 6 | "prettier.enable": false, 7 | "[typescript]": { 8 | "editor.defaultFormatter": "denoland.vscode-deno" 9 | }, 10 | "[json]": { 11 | "editor.defaultFormatter": "denoland.vscode-deno" 12 | }, 13 | "[prisma]": { 14 | "editor.defaultFormatter": "Prisma.prisma" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM denoland/deno:2.3.3 2 | 3 | WORKDIR /app 4 | COPY . . 5 | RUN deno install 6 | 7 | CMD ["deno", "task", "start"] 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Nx Custom Self-Hosted Remote Cache Server 2 | 3 | A Deno-based server implementation of the Nx Custom Self-Hosted Remote Cache 4 | specification. This server provides a caching layer for Nx build outputs using 5 | Amazon S3 as the storage backend. 6 | 7 | [![Deploy on Railway](https://railway.com/button.svg)](https://railway.com/template/-bmO7p?referralCode=73cYCO) 8 | 9 | ## Overview 10 | 11 | This server implements the 12 | [Nx Custom Remote Cache OpenAPI specification](https://nx.dev/recipes/running-tasks/self-hosted-caching#build-your-own-caching-server) 13 | and provides a production-ready solution for self-hosting your Nx remote cache. 14 | 15 | ## Features 16 | 17 | - Implements the Nx custom remote cache specification 18 | - Uses Amazon S3 for storage 19 | - Secure authentication using Bearer tokens 20 | - Efficient file streaming 21 | - Production-ready implementation 22 | - Available as a Docker image 23 | 24 | ## Prerequisites 25 | 26 | - [Deno](https://deno.land/) installed on your system 27 | - S3 compatible storage 28 | 29 | ## Environment Variables 30 | 31 | The following environment variables are required: 32 | 33 | ```env 34 | AWS_REGION=your-aws-region 35 | AWS_ACCESS_KEY_ID=your-access-key 36 | AWS_SECRET_ACCESS_KEY=your-secret-key 37 | S3_BUCKET_NAME=your-bucket-name 38 | S3_ENDPOINT_URL=your-s3-endpoint-url 39 | NX_CACHE_ACCESS_TOKEN=your-secure-token 40 | PORT=3000 # Optional, defaults to 3000 41 | ``` 42 | 43 | ## Installation 44 | 45 | ### Using Docker 46 | 47 | The easiest way to run the server is using the official Docker image: 48 | 49 | ```bash 50 | docker pull ghcr.io/ikatsuba/nx-cache-server:latest 51 | docker run -p 3000:3000 \ 52 | -e AWS_REGION=your-aws-region \ 53 | -e AWS_ACCESS_KEY_ID=your-access-key \ 54 | -e AWS_SECRET_ACCESS_KEY=your-secret-key \ 55 | -e S3_BUCKET_NAME=your-bucket-name \ 56 | -e S3_ENDPOINT_URL=your-s3-endpoint-url \ 57 | -e NX_CACHE_ACCESS_TOKEN=your-secure-token \ 58 | ghcr.io/ikatsuba/nx-cache-server:latest 59 | ``` 60 | 61 | ### Manual Installation 62 | 63 | 1. Clone the repository: 64 | 65 | ```bash 66 | git clone 67 | cd nx-cloud 68 | ``` 69 | 70 | 2. Run docker compose to start the MinIO server: 71 | 72 | ```bash 73 | docker compose up -d 74 | ``` 75 | 76 | ## Running the Server 77 | 78 | Start the server with: 79 | 80 | ```bash 81 | deno task start 82 | ``` 83 | 84 | ## Testing 85 | 86 | Run the tests with: 87 | 88 | ```bash 89 | deno task test 90 | deno task e2e 91 | ``` 92 | 93 | > **Note:** The tests assume that the MinIO server is running and that the 94 | > `nx-cloud` bucket exists. Be sure to run 95 | > `docker compose -f docker-compose.yml up s3 create_bucket_and_user -d` before 96 | > running the tests. 97 | 98 | ## Usage with Nx 99 | 100 | To use this cache server with your Nx workspace, set the following environment 101 | variables: 102 | 103 | ```bash 104 | NX_SELF_HOSTED_REMOTE_CACHE_SERVER=http://your-server:3000 105 | NX_SELF_HOSTED_REMOTE_CACHE_ACCESS_TOKEN=your-secure-token 106 | ``` 107 | 108 | ## Author 109 | 110 | - [Igor Katsuba](https://x.com/katsuba_igor) 111 | 112 | ## License 113 | 114 | MIT 115 | -------------------------------------------------------------------------------- /deno.json: -------------------------------------------------------------------------------- 1 | { 2 | "imports": { 3 | "@aws-sdk/client-s3": "npm:@aws-sdk/client-s3@^3.779.0", 4 | "@aws-sdk/s3-request-presigner": "npm:@aws-sdk/s3-request-presigner@^3.779.0", 5 | "@david/dax": "jsr:@david/dax@^0.43.0", 6 | "@std/assert": "jsr:@std/assert@^1.0.13", 7 | "@std/path": "jsr:@std/path@^1.0.9", 8 | "@std/testing": "jsr:@std/testing@^1.0.11", 9 | "hono": "npm:hono@^4.7.5" 10 | }, 11 | "fmt": { 12 | "singleQuote": true 13 | }, 14 | "tasks": { 15 | "start": "deno run --allow-env --allow-net --allow-sys --allow-read --env-file=.env src/index.ts", 16 | "dev": "deno run --watch --allow-env --allow-net --allow-sys --allow-read --env-file=.env.local src/index.ts", 17 | "test": "deno test --allow-env --allow-net --allow-sys --allow-read --env-file=.env.local src/**/*.test.ts", 18 | "e2e": "deno test --allow-env --allow-net --allow-sys --allow-read --allow-write --allow-run --env-file=.env.local e2e/**/*.test.ts", 19 | "wait-for-bucket": "deno run --allow-net --allow-env --env-file=.env.local --allow-sys --allow-read scripts/wait-for-bucket.ts" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /deno.lock: -------------------------------------------------------------------------------- 1 | { 2 | "version": "5", 3 | "specifiers": { 4 | "jsr:@david/dax@0.43": "0.43.0", 5 | "jsr:@david/path@0.2": "0.2.0", 6 | "jsr:@david/which@~0.4.1": "0.4.1", 7 | "jsr:@std/assert@*": "1.0.11", 8 | "jsr:@std/assert@^1.0.12": "1.0.13", 9 | "jsr:@std/assert@^1.0.13": "1.0.13", 10 | "jsr:@std/async@^1.0.12": "1.0.12", 11 | "jsr:@std/bytes@^1.0.5": "1.0.5", 12 | "jsr:@std/data-structures@^1.0.6": "1.0.7", 13 | "jsr:@std/fmt@1": "1.0.7", 14 | "jsr:@std/fs@1": "1.0.17", 15 | "jsr:@std/fs@^1.0.16": "1.0.17", 16 | "jsr:@std/internal@^1.0.5": "1.0.5", 17 | "jsr:@std/internal@^1.0.6": "1.0.6", 18 | "jsr:@std/io@0.225": "0.225.2", 19 | "jsr:@std/path@1": "1.0.9", 20 | "jsr:@std/path@^1.0.8": "1.0.9", 21 | "jsr:@std/path@^1.0.9": "1.0.9", 22 | "jsr:@std/testing@^1.0.11": "1.0.11", 23 | "npm:@aws-sdk/client-s3@*": "3.779.0", 24 | "npm:@aws-sdk/client-s3@^3.779.0": "3.779.0", 25 | "npm:@aws-sdk/s3-request-presigner@^3.779.0": "3.779.0", 26 | "npm:hono@^4.7.5": "4.7.5" 27 | }, 28 | "jsr": { 29 | "@david/dax@0.43.0": { 30 | "integrity": "72068d3b9eb00c08d70d0d1e2db8872402377279a0e6c88df172112eee1357f1", 31 | "dependencies": [ 32 | "jsr:@david/path", 33 | "jsr:@david/which", 34 | "jsr:@std/fmt", 35 | "jsr:@std/fs@1", 36 | "jsr:@std/io", 37 | "jsr:@std/path@1" 38 | ] 39 | }, 40 | "@david/path@0.2.0": { 41 | "integrity": "f2d7aa7f02ce5a55e27c09f9f1381794acb09d328f8d3c8a2e3ab3ffc294dccd", 42 | "dependencies": [ 43 | "jsr:@std/fs@1", 44 | "jsr:@std/path@1" 45 | ] 46 | }, 47 | "@david/which@0.4.1": { 48 | "integrity": "896a682b111f92ab866cc70c5b4afab2f5899d2f9bde31ed00203b9c250f225e" 49 | }, 50 | "@std/assert@1.0.11": { 51 | "integrity": "2461ef3c368fe88bc60e186e7744a93112f16fd110022e113a0849e94d1c83c1", 52 | "dependencies": [ 53 | "jsr:@std/internal@^1.0.5" 54 | ] 55 | }, 56 | "@std/assert@1.0.13": { 57 | "integrity": "ae0d31e41919b12c656c742b22522c32fb26ed0cba32975cb0de2a273cb68b29", 58 | "dependencies": [ 59 | "jsr:@std/internal@^1.0.6" 60 | ] 61 | }, 62 | "@std/async@1.0.12": { 63 | "integrity": "d1bfcec459e8012846fe4e38dfc4241ab23240ecda3d8d6dfcf6d81a632e803d" 64 | }, 65 | "@std/bytes@1.0.5": { 66 | "integrity": "4465dd739d7963d964c809202ebea6d5c6b8e3829ef25c6a224290fbb8a1021e" 67 | }, 68 | "@std/data-structures@1.0.7": { 69 | "integrity": "16932d2c8d281f65eaaa2209af2473209881e33b1ced54cd1b015e7b4cdbb0d2" 70 | }, 71 | "@std/fmt@1.0.7": { 72 | "integrity": "2a727c043d8df62cd0b819b3fb709b64dd622e42c3b1bb817ea7e6cc606360fb" 73 | }, 74 | "@std/fs@1.0.17": { 75 | "integrity": "1c00c632677c1158988ef7a004cb16137f870aafdb8163b9dce86ec652f3952b", 76 | "dependencies": [ 77 | "jsr:@std/path@^1.0.9" 78 | ] 79 | }, 80 | "@std/internal@1.0.5": { 81 | "integrity": "54a546004f769c1ac9e025abd15a76b6671ddc9687e2313b67376125650dc7ba" 82 | }, 83 | "@std/internal@1.0.6": { 84 | "integrity": "9533b128f230f73bd209408bb07a4b12f8d4255ab2a4d22a1fd6d87304aca9a4" 85 | }, 86 | "@std/io@0.225.2": { 87 | "integrity": "3c740cd4ee4c082e6cfc86458f47e2ab7cb353dc6234d5e9b1f91a2de5f4d6c7", 88 | "dependencies": [ 89 | "jsr:@std/bytes" 90 | ] 91 | }, 92 | "@std/path@1.0.9": { 93 | "integrity": "260a49f11edd3db93dd38350bf9cd1b4d1366afa98e81b86167b4e3dd750129e" 94 | }, 95 | "@std/testing@1.0.11": { 96 | "integrity": "12b3db12d34f0f385a26248933bde766c0f8c5ad8b6ab34d4d38f528ab852f48", 97 | "dependencies": [ 98 | "jsr:@std/assert@^1.0.12", 99 | "jsr:@std/async", 100 | "jsr:@std/data-structures", 101 | "jsr:@std/fs@^1.0.16", 102 | "jsr:@std/internal@^1.0.6", 103 | "jsr:@std/path@^1.0.8" 104 | ] 105 | } 106 | }, 107 | "npm": { 108 | "@aws-crypto/crc32@5.2.0": { 109 | "integrity": "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==", 110 | "dependencies": [ 111 | "@aws-crypto/util", 112 | "@aws-sdk/types", 113 | "tslib" 114 | ] 115 | }, 116 | "@aws-crypto/crc32c@5.2.0": { 117 | "integrity": "sha512-+iWb8qaHLYKrNvGRbiYRHSdKRWhto5XlZUEBwDjYNf+ly5SVYG6zEoYIdxvf5R3zyeP16w4PLBn3rH1xc74Rag==", 118 | "dependencies": [ 119 | "@aws-crypto/util", 120 | "@aws-sdk/types", 121 | "tslib" 122 | ] 123 | }, 124 | "@aws-crypto/sha1-browser@5.2.0": { 125 | "integrity": "sha512-OH6lveCFfcDjX4dbAvCFSYUjJZjDr/3XJ3xHtjn3Oj5b9RjojQo8npoLeA/bNwkOkrSQ0wgrHzXk4tDRxGKJeg==", 126 | "dependencies": [ 127 | "@aws-crypto/supports-web-crypto", 128 | "@aws-crypto/util", 129 | "@aws-sdk/types", 130 | "@aws-sdk/util-locate-window", 131 | "@smithy/util-utf8@2.3.0", 132 | "tslib" 133 | ] 134 | }, 135 | "@aws-crypto/sha256-browser@5.2.0": { 136 | "integrity": "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==", 137 | "dependencies": [ 138 | "@aws-crypto/sha256-js", 139 | "@aws-crypto/supports-web-crypto", 140 | "@aws-crypto/util", 141 | "@aws-sdk/types", 142 | "@aws-sdk/util-locate-window", 143 | "@smithy/util-utf8@2.3.0", 144 | "tslib" 145 | ] 146 | }, 147 | "@aws-crypto/sha256-js@5.2.0": { 148 | "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==", 149 | "dependencies": [ 150 | "@aws-crypto/util", 151 | "@aws-sdk/types", 152 | "tslib" 153 | ] 154 | }, 155 | "@aws-crypto/supports-web-crypto@5.2.0": { 156 | "integrity": "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==", 157 | "dependencies": [ 158 | "tslib" 159 | ] 160 | }, 161 | "@aws-crypto/util@5.2.0": { 162 | "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==", 163 | "dependencies": [ 164 | "@aws-sdk/types", 165 | "@smithy/util-utf8@2.3.0", 166 | "tslib" 167 | ] 168 | }, 169 | "@aws-sdk/client-s3@3.779.0": { 170 | "integrity": "sha512-Lagz+ersQaLNYkpOU9V12PYspT//lGvhPXlKU3OXDj3whDchdqUdtRKY8rmV+jli4KXe+udx/hj2yqrFRfKGvQ==", 171 | "dependencies": [ 172 | "@aws-crypto/sha1-browser", 173 | "@aws-crypto/sha256-browser", 174 | "@aws-crypto/sha256-js", 175 | "@aws-sdk/core", 176 | "@aws-sdk/credential-provider-node", 177 | "@aws-sdk/middleware-bucket-endpoint", 178 | "@aws-sdk/middleware-expect-continue", 179 | "@aws-sdk/middleware-flexible-checksums", 180 | "@aws-sdk/middleware-host-header", 181 | "@aws-sdk/middleware-location-constraint", 182 | "@aws-sdk/middleware-logger", 183 | "@aws-sdk/middleware-recursion-detection", 184 | "@aws-sdk/middleware-sdk-s3", 185 | "@aws-sdk/middleware-ssec", 186 | "@aws-sdk/middleware-user-agent", 187 | "@aws-sdk/region-config-resolver", 188 | "@aws-sdk/signature-v4-multi-region", 189 | "@aws-sdk/types", 190 | "@aws-sdk/util-endpoints", 191 | "@aws-sdk/util-user-agent-browser", 192 | "@aws-sdk/util-user-agent-node", 193 | "@aws-sdk/xml-builder", 194 | "@smithy/config-resolver", 195 | "@smithy/core", 196 | "@smithy/eventstream-serde-browser", 197 | "@smithy/eventstream-serde-config-resolver", 198 | "@smithy/eventstream-serde-node", 199 | "@smithy/fetch-http-handler", 200 | "@smithy/hash-blob-browser", 201 | "@smithy/hash-node", 202 | "@smithy/hash-stream-node", 203 | "@smithy/invalid-dependency", 204 | "@smithy/md5-js", 205 | "@smithy/middleware-content-length", 206 | "@smithy/middleware-endpoint", 207 | "@smithy/middleware-retry", 208 | "@smithy/middleware-serde", 209 | "@smithy/middleware-stack", 210 | "@smithy/node-config-provider", 211 | "@smithy/node-http-handler", 212 | "@smithy/protocol-http", 213 | "@smithy/smithy-client", 214 | "@smithy/types", 215 | "@smithy/url-parser", 216 | "@smithy/util-base64", 217 | "@smithy/util-body-length-browser", 218 | "@smithy/util-body-length-node", 219 | "@smithy/util-defaults-mode-browser", 220 | "@smithy/util-defaults-mode-node", 221 | "@smithy/util-endpoints", 222 | "@smithy/util-middleware", 223 | "@smithy/util-retry", 224 | "@smithy/util-stream", 225 | "@smithy/util-utf8@4.0.0", 226 | "@smithy/util-waiter", 227 | "tslib" 228 | ] 229 | }, 230 | "@aws-sdk/client-sso@3.777.0": { 231 | "integrity": "sha512-0+z6CiAYIQa7s6FJ+dpBYPi9zr9yY5jBg/4/FGcwYbmqWPXwL9Thdtr0FearYRZgKl7bhL3m3dILCCfWqr3teQ==", 232 | "dependencies": [ 233 | "@aws-crypto/sha256-browser", 234 | "@aws-crypto/sha256-js", 235 | "@aws-sdk/core", 236 | "@aws-sdk/middleware-host-header", 237 | "@aws-sdk/middleware-logger", 238 | "@aws-sdk/middleware-recursion-detection", 239 | "@aws-sdk/middleware-user-agent", 240 | "@aws-sdk/region-config-resolver", 241 | "@aws-sdk/types", 242 | "@aws-sdk/util-endpoints", 243 | "@aws-sdk/util-user-agent-browser", 244 | "@aws-sdk/util-user-agent-node", 245 | "@smithy/config-resolver", 246 | "@smithy/core", 247 | "@smithy/fetch-http-handler", 248 | "@smithy/hash-node", 249 | "@smithy/invalid-dependency", 250 | "@smithy/middleware-content-length", 251 | "@smithy/middleware-endpoint", 252 | "@smithy/middleware-retry", 253 | "@smithy/middleware-serde", 254 | "@smithy/middleware-stack", 255 | "@smithy/node-config-provider", 256 | "@smithy/node-http-handler", 257 | "@smithy/protocol-http", 258 | "@smithy/smithy-client", 259 | "@smithy/types", 260 | "@smithy/url-parser", 261 | "@smithy/util-base64", 262 | "@smithy/util-body-length-browser", 263 | "@smithy/util-body-length-node", 264 | "@smithy/util-defaults-mode-browser", 265 | "@smithy/util-defaults-mode-node", 266 | "@smithy/util-endpoints", 267 | "@smithy/util-middleware", 268 | "@smithy/util-retry", 269 | "@smithy/util-utf8@4.0.0", 270 | "tslib" 271 | ] 272 | }, 273 | "@aws-sdk/core@3.775.0": { 274 | "integrity": "sha512-8vpW4WihVfz0DX+7WnnLGm3GuQER++b0IwQG35JlQMlgqnc44M//KbJPsIHA0aJUJVwJAEShgfr5dUbY8WUzaA==", 275 | "dependencies": [ 276 | "@aws-sdk/types", 277 | "@smithy/core", 278 | "@smithy/node-config-provider", 279 | "@smithy/property-provider", 280 | "@smithy/protocol-http", 281 | "@smithy/signature-v4", 282 | "@smithy/smithy-client", 283 | "@smithy/types", 284 | "@smithy/util-middleware", 285 | "fast-xml-parser", 286 | "tslib" 287 | ] 288 | }, 289 | "@aws-sdk/credential-provider-env@3.775.0": { 290 | "integrity": "sha512-6ESVxwCbGm7WZ17kY1fjmxQud43vzJFoLd4bmlR+idQSWdqlzGDYdcfzpjDKTcivdtNrVYmFvcH1JBUwCRAZhw==", 291 | "dependencies": [ 292 | "@aws-sdk/core", 293 | "@aws-sdk/types", 294 | "@smithy/property-provider", 295 | "@smithy/types", 296 | "tslib" 297 | ] 298 | }, 299 | "@aws-sdk/credential-provider-http@3.775.0": { 300 | "integrity": "sha512-PjDQeDH/J1S0yWV32wCj2k5liRo0ssXMseCBEkCsD3SqsU8o5cU82b0hMX4sAib/RkglCSZqGO0xMiN0/7ndww==", 301 | "dependencies": [ 302 | "@aws-sdk/core", 303 | "@aws-sdk/types", 304 | "@smithy/fetch-http-handler", 305 | "@smithy/node-http-handler", 306 | "@smithy/property-provider", 307 | "@smithy/protocol-http", 308 | "@smithy/smithy-client", 309 | "@smithy/types", 310 | "@smithy/util-stream", 311 | "tslib" 312 | ] 313 | }, 314 | "@aws-sdk/credential-provider-ini@3.777.0": { 315 | "integrity": "sha512-1X9mCuM9JSQPmQ+D2TODt4THy6aJWCNiURkmKmTIPRdno7EIKgAqrr/LLN++K5mBf54DZVKpqcJutXU2jwo01A==", 316 | "dependencies": [ 317 | "@aws-sdk/core", 318 | "@aws-sdk/credential-provider-env", 319 | "@aws-sdk/credential-provider-http", 320 | "@aws-sdk/credential-provider-process", 321 | "@aws-sdk/credential-provider-sso", 322 | "@aws-sdk/credential-provider-web-identity", 323 | "@aws-sdk/nested-clients", 324 | "@aws-sdk/types", 325 | "@smithy/credential-provider-imds", 326 | "@smithy/property-provider", 327 | "@smithy/shared-ini-file-loader", 328 | "@smithy/types", 329 | "tslib" 330 | ] 331 | }, 332 | "@aws-sdk/credential-provider-node@3.777.0": { 333 | "integrity": "sha512-ZD66ywx1Q0KyUSuBXZIQzBe3Q7MzX8lNwsrCU43H3Fww+Y+HB3Ncws9grhSdNhKQNeGmZ+MgKybuZYaaeLwJEQ==", 334 | "dependencies": [ 335 | "@aws-sdk/credential-provider-env", 336 | "@aws-sdk/credential-provider-http", 337 | "@aws-sdk/credential-provider-ini", 338 | "@aws-sdk/credential-provider-process", 339 | "@aws-sdk/credential-provider-sso", 340 | "@aws-sdk/credential-provider-web-identity", 341 | "@aws-sdk/types", 342 | "@smithy/credential-provider-imds", 343 | "@smithy/property-provider", 344 | "@smithy/shared-ini-file-loader", 345 | "@smithy/types", 346 | "tslib" 347 | ] 348 | }, 349 | "@aws-sdk/credential-provider-process@3.775.0": { 350 | "integrity": "sha512-A6k68H9rQp+2+7P7SGO90Csw6nrUEm0Qfjpn9Etc4EboZhhCLs9b66umUsTsSBHus4FDIe5JQxfCUyt1wgNogg==", 351 | "dependencies": [ 352 | "@aws-sdk/core", 353 | "@aws-sdk/types", 354 | "@smithy/property-provider", 355 | "@smithy/shared-ini-file-loader", 356 | "@smithy/types", 357 | "tslib" 358 | ] 359 | }, 360 | "@aws-sdk/credential-provider-sso@3.777.0": { 361 | "integrity": "sha512-9mPz7vk9uE4PBVprfINv4tlTkyq1OonNevx2DiXC1LY4mCUCNN3RdBwAY0BTLzj0uyc3k5KxFFNbn3/8ZDQP7w==", 362 | "dependencies": [ 363 | "@aws-sdk/client-sso", 364 | "@aws-sdk/core", 365 | "@aws-sdk/token-providers", 366 | "@aws-sdk/types", 367 | "@smithy/property-provider", 368 | "@smithy/shared-ini-file-loader", 369 | "@smithy/types", 370 | "tslib" 371 | ] 372 | }, 373 | "@aws-sdk/credential-provider-web-identity@3.777.0": { 374 | "integrity": "sha512-uGCqr47fnthkqwq5luNl2dksgcpHHjSXz2jUra7TXtFOpqvnhOW8qXjoa1ivlkq8qhqlaZwCzPdbcN0lXpmLzQ==", 375 | "dependencies": [ 376 | "@aws-sdk/core", 377 | "@aws-sdk/nested-clients", 378 | "@aws-sdk/types", 379 | "@smithy/property-provider", 380 | "@smithy/types", 381 | "tslib" 382 | ] 383 | }, 384 | "@aws-sdk/middleware-bucket-endpoint@3.775.0": { 385 | "integrity": "sha512-qogMIpVChDYr4xiUNC19/RDSw/sKoHkAhouS6Skxiy6s27HBhow1L3Z1qVYXuBmOZGSWPU0xiyZCvOyWrv9s+Q==", 386 | "dependencies": [ 387 | "@aws-sdk/types", 388 | "@aws-sdk/util-arn-parser", 389 | "@smithy/node-config-provider", 390 | "@smithy/protocol-http", 391 | "@smithy/types", 392 | "@smithy/util-config-provider", 393 | "tslib" 394 | ] 395 | }, 396 | "@aws-sdk/middleware-expect-continue@3.775.0": { 397 | "integrity": "sha512-Apd3owkIeUW5dnk3au9np2IdW2N0zc9NjTjHiH+Mx3zqwSrc+m+ANgJVgk9mnQjMzU/vb7VuxJ0eqdEbp5gYsg==", 398 | "dependencies": [ 399 | "@aws-sdk/types", 400 | "@smithy/protocol-http", 401 | "@smithy/types", 402 | "tslib" 403 | ] 404 | }, 405 | "@aws-sdk/middleware-flexible-checksums@3.775.0": { 406 | "integrity": "sha512-OmHLfRIb7IIXsf9/X/pMOlcSV3gzW/MmtPSZTkrz5jCTKzWXd7eRoyOJqewjsaC6KMAxIpNU77FoAd16jOZ21A==", 407 | "dependencies": [ 408 | "@aws-crypto/crc32", 409 | "@aws-crypto/crc32c", 410 | "@aws-crypto/util", 411 | "@aws-sdk/core", 412 | "@aws-sdk/types", 413 | "@smithy/is-array-buffer@4.0.0", 414 | "@smithy/node-config-provider", 415 | "@smithy/protocol-http", 416 | "@smithy/types", 417 | "@smithy/util-middleware", 418 | "@smithy/util-stream", 419 | "@smithy/util-utf8@4.0.0", 420 | "tslib" 421 | ] 422 | }, 423 | "@aws-sdk/middleware-host-header@3.775.0": { 424 | "integrity": "sha512-tkSegM0Z6WMXpLB8oPys/d+umYIocvO298mGvcMCncpRl77L9XkvSLJIFzaHes+o7djAgIduYw8wKIMStFss2w==", 425 | "dependencies": [ 426 | "@aws-sdk/types", 427 | "@smithy/protocol-http", 428 | "@smithy/types", 429 | "tslib" 430 | ] 431 | }, 432 | "@aws-sdk/middleware-location-constraint@3.775.0": { 433 | "integrity": "sha512-8TMXEHZXZTFTckQLyBT5aEI8fX11HZcwZseRifvBKKpj0RZDk4F0EEYGxeNSPpUQ7n+PRWyfAEnnZNRdAj/1NQ==", 434 | "dependencies": [ 435 | "@aws-sdk/types", 436 | "@smithy/types", 437 | "tslib" 438 | ] 439 | }, 440 | "@aws-sdk/middleware-logger@3.775.0": { 441 | "integrity": "sha512-FaxO1xom4MAoUJsldmR92nT1G6uZxTdNYOFYtdHfd6N2wcNaTuxgjIvqzg5y7QIH9kn58XX/dzf1iTjgqUStZw==", 442 | "dependencies": [ 443 | "@aws-sdk/types", 444 | "@smithy/types", 445 | "tslib" 446 | ] 447 | }, 448 | "@aws-sdk/middleware-recursion-detection@3.775.0": { 449 | "integrity": "sha512-GLCzC8D0A0YDG5u3F5U03Vb9j5tcOEFhr8oc6PDk0k0vm5VwtZOE6LvK7hcCSoAB4HXyOUM0sQuXrbaAh9OwXA==", 450 | "dependencies": [ 451 | "@aws-sdk/types", 452 | "@smithy/protocol-http", 453 | "@smithy/types", 454 | "tslib" 455 | ] 456 | }, 457 | "@aws-sdk/middleware-sdk-s3@3.775.0": { 458 | "integrity": "sha512-zsvcu7cWB28JJ60gVvjxPCI7ZU7jWGcpNACPiZGyVtjYXwcxyhXbYEVDSWKsSA6ERpz9XrpLYod8INQWfW3ECg==", 459 | "dependencies": [ 460 | "@aws-sdk/core", 461 | "@aws-sdk/types", 462 | "@aws-sdk/util-arn-parser", 463 | "@smithy/core", 464 | "@smithy/node-config-provider", 465 | "@smithy/protocol-http", 466 | "@smithy/signature-v4", 467 | "@smithy/smithy-client", 468 | "@smithy/types", 469 | "@smithy/util-config-provider", 470 | "@smithy/util-middleware", 471 | "@smithy/util-stream", 472 | "@smithy/util-utf8@4.0.0", 473 | "tslib" 474 | ] 475 | }, 476 | "@aws-sdk/middleware-ssec@3.775.0": { 477 | "integrity": "sha512-Iw1RHD8vfAWWPzBBIKaojO4GAvQkHOYIpKdAfis/EUSUmSa79QsnXnRqsdcE0mCB0Ylj23yi+ah4/0wh9FsekA==", 478 | "dependencies": [ 479 | "@aws-sdk/types", 480 | "@smithy/types", 481 | "tslib" 482 | ] 483 | }, 484 | "@aws-sdk/middleware-user-agent@3.775.0": { 485 | "integrity": "sha512-7Lffpr1ptOEDE1ZYH1T78pheEY1YmeXWBfFt/amZ6AGsKSLG+JPXvof3ltporTGR2bhH/eJPo7UHCglIuXfzYg==", 486 | "dependencies": [ 487 | "@aws-sdk/core", 488 | "@aws-sdk/types", 489 | "@aws-sdk/util-endpoints", 490 | "@smithy/core", 491 | "@smithy/protocol-http", 492 | "@smithy/types", 493 | "tslib" 494 | ] 495 | }, 496 | "@aws-sdk/nested-clients@3.777.0": { 497 | "integrity": "sha512-bmmVRsCjuYlStYPt06hr+f8iEyWg7+AklKCA8ZLDEJujXhXIowgUIqXmqpTkXwkVvDQ9tzU7hxaONjyaQCGybA==", 498 | "dependencies": [ 499 | "@aws-crypto/sha256-browser", 500 | "@aws-crypto/sha256-js", 501 | "@aws-sdk/core", 502 | "@aws-sdk/middleware-host-header", 503 | "@aws-sdk/middleware-logger", 504 | "@aws-sdk/middleware-recursion-detection", 505 | "@aws-sdk/middleware-user-agent", 506 | "@aws-sdk/region-config-resolver", 507 | "@aws-sdk/types", 508 | "@aws-sdk/util-endpoints", 509 | "@aws-sdk/util-user-agent-browser", 510 | "@aws-sdk/util-user-agent-node", 511 | "@smithy/config-resolver", 512 | "@smithy/core", 513 | "@smithy/fetch-http-handler", 514 | "@smithy/hash-node", 515 | "@smithy/invalid-dependency", 516 | "@smithy/middleware-content-length", 517 | "@smithy/middleware-endpoint", 518 | "@smithy/middleware-retry", 519 | "@smithy/middleware-serde", 520 | "@smithy/middleware-stack", 521 | "@smithy/node-config-provider", 522 | "@smithy/node-http-handler", 523 | "@smithy/protocol-http", 524 | "@smithy/smithy-client", 525 | "@smithy/types", 526 | "@smithy/url-parser", 527 | "@smithy/util-base64", 528 | "@smithy/util-body-length-browser", 529 | "@smithy/util-body-length-node", 530 | "@smithy/util-defaults-mode-browser", 531 | "@smithy/util-defaults-mode-node", 532 | "@smithy/util-endpoints", 533 | "@smithy/util-middleware", 534 | "@smithy/util-retry", 535 | "@smithy/util-utf8@4.0.0", 536 | "tslib" 537 | ] 538 | }, 539 | "@aws-sdk/region-config-resolver@3.775.0": { 540 | "integrity": "sha512-40iH3LJjrQS3LKUJAl7Wj0bln7RFPEvUYKFxtP8a+oKFDO0F65F52xZxIJbPn6sHkxWDAnZlGgdjZXM3p2g5wQ==", 541 | "dependencies": [ 542 | "@aws-sdk/types", 543 | "@smithy/node-config-provider", 544 | "@smithy/types", 545 | "@smithy/util-config-provider", 546 | "@smithy/util-middleware", 547 | "tslib" 548 | ] 549 | }, 550 | "@aws-sdk/s3-request-presigner@3.779.0": { 551 | "integrity": "sha512-L3mGSh6/9gf3FBVrQziCkuLbaRJMeNbLr6tg9ZSymJcDRzRqAiCWnHrenAavTnAAnm+Lu62Fg/A4g3T+YT+gEg==", 552 | "dependencies": [ 553 | "@aws-sdk/signature-v4-multi-region", 554 | "@aws-sdk/types", 555 | "@aws-sdk/util-format-url", 556 | "@smithy/middleware-endpoint", 557 | "@smithy/protocol-http", 558 | "@smithy/smithy-client", 559 | "@smithy/types", 560 | "tslib" 561 | ] 562 | }, 563 | "@aws-sdk/signature-v4-multi-region@3.775.0": { 564 | "integrity": "sha512-cnGk8GDfTMJ8p7+qSk92QlIk2bmTmFJqhYxcXZ9PysjZtx0xmfCMxnG3Hjy1oU2mt5boPCVSOptqtWixayM17g==", 565 | "dependencies": [ 566 | "@aws-sdk/middleware-sdk-s3", 567 | "@aws-sdk/types", 568 | "@smithy/protocol-http", 569 | "@smithy/signature-v4", 570 | "@smithy/types", 571 | "tslib" 572 | ] 573 | }, 574 | "@aws-sdk/token-providers@3.777.0": { 575 | "integrity": "sha512-Yc2cDONsHOa4dTSGOev6Ng2QgTKQUEjaUnsyKd13pc/nLLz/WLqHiQ/o7PcnKERJxXGs1g1C6l3sNXiX+kbnFQ==", 576 | "dependencies": [ 577 | "@aws-sdk/nested-clients", 578 | "@aws-sdk/types", 579 | "@smithy/property-provider", 580 | "@smithy/shared-ini-file-loader", 581 | "@smithy/types", 582 | "tslib" 583 | ] 584 | }, 585 | "@aws-sdk/types@3.775.0": { 586 | "integrity": "sha512-ZoGKwa4C9fC9Av6bdfqcW6Ix5ot05F/S4VxWR2nHuMv7hzfmAjTOcUiWT7UR4hM/U0whf84VhDtXN/DWAk52KA==", 587 | "dependencies": [ 588 | "@smithy/types", 589 | "tslib" 590 | ] 591 | }, 592 | "@aws-sdk/util-arn-parser@3.723.0": { 593 | "integrity": "sha512-ZhEfvUwNliOQROcAk34WJWVYTlTa4694kSVhDSjW6lE1bMataPnIN8A0ycukEzBXmd8ZSoBcQLn6lKGl7XIJ5w==", 594 | "dependencies": [ 595 | "tslib" 596 | ] 597 | }, 598 | "@aws-sdk/util-endpoints@3.775.0": { 599 | "integrity": "sha512-yjWmUgZC9tUxAo8Uaplqmq0eUh0zrbZJdwxGRKdYxfm4RG6fMw1tj52+KkatH7o+mNZvg1GDcVp/INktxonJLw==", 600 | "dependencies": [ 601 | "@aws-sdk/types", 602 | "@smithy/types", 603 | "@smithy/util-endpoints", 604 | "tslib" 605 | ] 606 | }, 607 | "@aws-sdk/util-format-url@3.775.0": { 608 | "integrity": "sha512-Nw4nBeyCbWixoGh8NcVpa/i8McMA6RXJIjQFyloJLaPr7CPquz7ZbSl0MUWMFVwP/VHaJ7B+lNN3Qz1iFCEP/Q==", 609 | "dependencies": [ 610 | "@aws-sdk/types", 611 | "@smithy/querystring-builder", 612 | "@smithy/types", 613 | "tslib" 614 | ] 615 | }, 616 | "@aws-sdk/util-locate-window@3.723.0": { 617 | "integrity": "sha512-Yf2CS10BqK688DRsrKI/EO6B8ff5J86NXe4C+VCysK7UOgN0l1zOTeTukZ3H8Q9tYYX3oaF1961o8vRkFm7Nmw==", 618 | "dependencies": [ 619 | "tslib" 620 | ] 621 | }, 622 | "@aws-sdk/util-user-agent-browser@3.775.0": { 623 | "integrity": "sha512-txw2wkiJmZKVdDbscK7VBK+u+TJnRtlUjRTLei+elZg2ADhpQxfVAQl436FUeIv6AhB/oRHW6/K/EAGXUSWi0A==", 624 | "dependencies": [ 625 | "@aws-sdk/types", 626 | "@smithy/types", 627 | "bowser", 628 | "tslib" 629 | ] 630 | }, 631 | "@aws-sdk/util-user-agent-node@3.775.0": { 632 | "integrity": "sha512-N9yhTevbizTOMo3drH7Eoy6OkJ3iVPxhV7dwb6CMAObbLneS36CSfA6xQXupmHWcRvZPTz8rd1JGG3HzFOau+g==", 633 | "dependencies": [ 634 | "@aws-sdk/middleware-user-agent", 635 | "@aws-sdk/types", 636 | "@smithy/node-config-provider", 637 | "@smithy/types", 638 | "tslib" 639 | ] 640 | }, 641 | "@aws-sdk/xml-builder@3.775.0": { 642 | "integrity": "sha512-b9NGO6FKJeLGYnV7Z1yvcP1TNU4dkD5jNsLWOF1/sygZoASaQhNOlaiJ/1OH331YQ1R1oWk38nBb0frsYkDsOQ==", 643 | "dependencies": [ 644 | "@smithy/types", 645 | "tslib" 646 | ] 647 | }, 648 | "@smithy/abort-controller@4.0.2": { 649 | "integrity": "sha512-Sl/78VDtgqKxN2+1qduaVE140XF+Xg+TafkncspwM4jFP/LHr76ZHmIY/y3V1M0mMLNk+Je6IGbzxy23RSToMw==", 650 | "dependencies": [ 651 | "@smithy/types", 652 | "tslib" 653 | ] 654 | }, 655 | "@smithy/chunked-blob-reader-native@4.0.0": { 656 | "integrity": "sha512-R9wM2yPmfEMsUmlMlIgSzOyICs0x9uu7UTHoccMyt7BWw8shcGM8HqB355+BZCPBcySvbTYMs62EgEQkNxz2ig==", 657 | "dependencies": [ 658 | "@smithy/util-base64", 659 | "tslib" 660 | ] 661 | }, 662 | "@smithy/chunked-blob-reader@5.0.0": { 663 | "integrity": "sha512-+sKqDBQqb036hh4NPaUiEkYFkTUGYzRsn3EuFhyfQfMy6oGHEUJDurLP9Ufb5dasr/XiAmPNMr6wa9afjQB+Gw==", 664 | "dependencies": [ 665 | "tslib" 666 | ] 667 | }, 668 | "@smithy/config-resolver@4.1.0": { 669 | "integrity": "sha512-8smPlwhga22pwl23fM5ew4T9vfLUCeFXlcqNOCD5M5h8VmNPNUE9j6bQSuRXpDSV11L/E/SwEBQuW8hr6+nS1A==", 670 | "dependencies": [ 671 | "@smithy/node-config-provider", 672 | "@smithy/types", 673 | "@smithy/util-config-provider", 674 | "@smithy/util-middleware", 675 | "tslib" 676 | ] 677 | }, 678 | "@smithy/core@3.2.0": { 679 | "integrity": "sha512-k17bgQhVZ7YmUvA8at4af1TDpl0NDMBuBKJl8Yg0nrefwmValU+CnA5l/AriVdQNthU/33H3nK71HrLgqOPr1Q==", 680 | "dependencies": [ 681 | "@smithy/middleware-serde", 682 | "@smithy/protocol-http", 683 | "@smithy/types", 684 | "@smithy/util-body-length-browser", 685 | "@smithy/util-middleware", 686 | "@smithy/util-stream", 687 | "@smithy/util-utf8@4.0.0", 688 | "tslib" 689 | ] 690 | }, 691 | "@smithy/credential-provider-imds@4.0.2": { 692 | "integrity": "sha512-32lVig6jCaWBHnY+OEQ6e6Vnt5vDHaLiydGrwYMW9tPqO688hPGTYRamYJ1EptxEC2rAwJrHWmPoKRBl4iTa8w==", 693 | "dependencies": [ 694 | "@smithy/node-config-provider", 695 | "@smithy/property-provider", 696 | "@smithy/types", 697 | "@smithy/url-parser", 698 | "tslib" 699 | ] 700 | }, 701 | "@smithy/eventstream-codec@4.0.2": { 702 | "integrity": "sha512-p+f2kLSK7ZrXVfskU/f5dzksKTewZk8pJLPvER3aFHPt76C2MxD9vNatSfLzzQSQB4FNO96RK4PSXfhD1TTeMQ==", 703 | "dependencies": [ 704 | "@aws-crypto/crc32", 705 | "@smithy/types", 706 | "@smithy/util-hex-encoding", 707 | "tslib" 708 | ] 709 | }, 710 | "@smithy/eventstream-serde-browser@4.0.2": { 711 | "integrity": "sha512-CepZCDs2xgVUtH7ZZ7oDdZFH8e6Y2zOv8iiX6RhndH69nlojCALSKK+OXwZUgOtUZEUaZ5e1hULVCHYbCn7pug==", 712 | "dependencies": [ 713 | "@smithy/eventstream-serde-universal", 714 | "@smithy/types", 715 | "tslib" 716 | ] 717 | }, 718 | "@smithy/eventstream-serde-config-resolver@4.1.0": { 719 | "integrity": "sha512-1PI+WPZ5TWXrfj3CIoKyUycYynYJgZjuQo8U+sphneOtjsgrttYybdqESFReQrdWJ+LKt6NEdbYzmmfDBmjX2A==", 720 | "dependencies": [ 721 | "@smithy/types", 722 | "tslib" 723 | ] 724 | }, 725 | "@smithy/eventstream-serde-node@4.0.2": { 726 | "integrity": "sha512-C5bJ/C6x9ENPMx2cFOirspnF9ZsBVnBMtP6BdPl/qYSuUawdGQ34Lq0dMcf42QTjUZgWGbUIZnz6+zLxJlb9aw==", 727 | "dependencies": [ 728 | "@smithy/eventstream-serde-universal", 729 | "@smithy/types", 730 | "tslib" 731 | ] 732 | }, 733 | "@smithy/eventstream-serde-universal@4.0.2": { 734 | "integrity": "sha512-St8h9JqzvnbB52FtckiHPN4U/cnXcarMniXRXTKn0r4b4XesZOGiAyUdj1aXbqqn1icSqBlzzUsCl6nPB018ng==", 735 | "dependencies": [ 736 | "@smithy/eventstream-codec", 737 | "@smithy/types", 738 | "tslib" 739 | ] 740 | }, 741 | "@smithy/fetch-http-handler@5.0.2": { 742 | "integrity": "sha512-+9Dz8sakS9pe7f2cBocpJXdeVjMopUDLgZs1yWeu7h++WqSbjUYv/JAJwKwXw1HV6gq1jyWjxuyn24E2GhoEcQ==", 743 | "dependencies": [ 744 | "@smithy/protocol-http", 745 | "@smithy/querystring-builder", 746 | "@smithy/types", 747 | "@smithy/util-base64", 748 | "tslib" 749 | ] 750 | }, 751 | "@smithy/hash-blob-browser@4.0.2": { 752 | "integrity": "sha512-3g188Z3DyhtzfBRxpZjU8R9PpOQuYsbNnyStc/ZVS+9nVX1f6XeNOa9IrAh35HwwIZg+XWk8bFVtNINVscBP+g==", 753 | "dependencies": [ 754 | "@smithy/chunked-blob-reader", 755 | "@smithy/chunked-blob-reader-native", 756 | "@smithy/types", 757 | "tslib" 758 | ] 759 | }, 760 | "@smithy/hash-node@4.0.2": { 761 | "integrity": "sha512-VnTpYPnRUE7yVhWozFdlxcYknv9UN7CeOqSrMH+V877v4oqtVYuoqhIhtSjmGPvYrYnAkaM61sLMKHvxL138yg==", 762 | "dependencies": [ 763 | "@smithy/types", 764 | "@smithy/util-buffer-from@4.0.0", 765 | "@smithy/util-utf8@4.0.0", 766 | "tslib" 767 | ] 768 | }, 769 | "@smithy/hash-stream-node@4.0.2": { 770 | "integrity": "sha512-POWDuTznzbIwlEXEvvXoPMS10y0WKXK790soe57tFRfvf4zBHyzE529HpZMqmDdwG9MfFflnyzndUQ8j78ZdSg==", 771 | "dependencies": [ 772 | "@smithy/types", 773 | "@smithy/util-utf8@4.0.0", 774 | "tslib" 775 | ] 776 | }, 777 | "@smithy/invalid-dependency@4.0.2": { 778 | "integrity": "sha512-GatB4+2DTpgWPday+mnUkoumP54u/MDM/5u44KF9hIu8jF0uafZtQLcdfIKkIcUNuF/fBojpLEHZS/56JqPeXQ==", 779 | "dependencies": [ 780 | "@smithy/types", 781 | "tslib" 782 | ] 783 | }, 784 | "@smithy/is-array-buffer@2.2.0": { 785 | "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", 786 | "dependencies": [ 787 | "tslib" 788 | ] 789 | }, 790 | "@smithy/is-array-buffer@4.0.0": { 791 | "integrity": "sha512-saYhF8ZZNoJDTvJBEWgeBccCg+yvp1CX+ed12yORU3NilJScfc6gfch2oVb4QgxZrGUx3/ZJlb+c/dJbyupxlw==", 792 | "dependencies": [ 793 | "tslib" 794 | ] 795 | }, 796 | "@smithy/md5-js@4.0.2": { 797 | "integrity": "sha512-Hc0R8EiuVunUewCse2syVgA2AfSRco3LyAv07B/zCOMa+jpXI9ll+Q21Nc6FAlYPcpNcAXqBzMhNs1CD/pP2bA==", 798 | "dependencies": [ 799 | "@smithy/types", 800 | "@smithy/util-utf8@4.0.0", 801 | "tslib" 802 | ] 803 | }, 804 | "@smithy/middleware-content-length@4.0.2": { 805 | "integrity": "sha512-hAfEXm1zU+ELvucxqQ7I8SszwQ4znWMbNv6PLMndN83JJN41EPuS93AIyh2N+gJ6x8QFhzSO6b7q2e6oClDI8A==", 806 | "dependencies": [ 807 | "@smithy/protocol-http", 808 | "@smithy/types", 809 | "tslib" 810 | ] 811 | }, 812 | "@smithy/middleware-endpoint@4.1.0": { 813 | "integrity": "sha512-xhLimgNCbCzsUppRTGXWkZywksuTThxaIB0HwbpsVLY5sceac4e1TZ/WKYqufQLaUy+gUSJGNdwD2jo3cXL0iA==", 814 | "dependencies": [ 815 | "@smithy/core", 816 | "@smithy/middleware-serde", 817 | "@smithy/node-config-provider", 818 | "@smithy/shared-ini-file-loader", 819 | "@smithy/types", 820 | "@smithy/url-parser", 821 | "@smithy/util-middleware", 822 | "tslib" 823 | ] 824 | }, 825 | "@smithy/middleware-retry@4.1.0": { 826 | "integrity": "sha512-2zAagd1s6hAaI/ap6SXi5T3dDwBOczOMCSkkYzktqN1+tzbk1GAsHNAdo/1uzxz3Ky02jvZQwbi/vmDA6z4Oyg==", 827 | "dependencies": [ 828 | "@smithy/node-config-provider", 829 | "@smithy/protocol-http", 830 | "@smithy/service-error-classification", 831 | "@smithy/smithy-client", 832 | "@smithy/types", 833 | "@smithy/util-middleware", 834 | "@smithy/util-retry", 835 | "tslib", 836 | "uuid" 837 | ] 838 | }, 839 | "@smithy/middleware-serde@4.0.3": { 840 | "integrity": "sha512-rfgDVrgLEVMmMn0BI8O+8OVr6vXzjV7HZj57l0QxslhzbvVfikZbVfBVthjLHqib4BW44QhcIgJpvebHlRaC9A==", 841 | "dependencies": [ 842 | "@smithy/types", 843 | "tslib" 844 | ] 845 | }, 846 | "@smithy/middleware-stack@4.0.2": { 847 | "integrity": "sha512-eSPVcuJJGVYrFYu2hEq8g8WWdJav3sdrI4o2c6z/rjnYDd3xH9j9E7deZQCzFn4QvGPouLngH3dQ+QVTxv5bOQ==", 848 | "dependencies": [ 849 | "@smithy/types", 850 | "tslib" 851 | ] 852 | }, 853 | "@smithy/node-config-provider@4.0.2": { 854 | "integrity": "sha512-WgCkILRZfJwJ4Da92a6t3ozN/zcvYyJGUTmfGbgS/FkCcoCjl7G4FJaCDN1ySdvLvemnQeo25FdkyMSTSwulsw==", 855 | "dependencies": [ 856 | "@smithy/property-provider", 857 | "@smithy/shared-ini-file-loader", 858 | "@smithy/types", 859 | "tslib" 860 | ] 861 | }, 862 | "@smithy/node-http-handler@4.0.4": { 863 | "integrity": "sha512-/mdqabuAT3o/ihBGjL94PUbTSPSRJ0eeVTdgADzow0wRJ0rN4A27EOrtlK56MYiO1fDvlO3jVTCxQtQmK9dZ1g==", 864 | "dependencies": [ 865 | "@smithy/abort-controller", 866 | "@smithy/protocol-http", 867 | "@smithy/querystring-builder", 868 | "@smithy/types", 869 | "tslib" 870 | ] 871 | }, 872 | "@smithy/property-provider@4.0.2": { 873 | "integrity": "sha512-wNRoQC1uISOuNc2s4hkOYwYllmiyrvVXWMtq+TysNRVQaHm4yoafYQyjN/goYZS+QbYlPIbb/QRjaUZMuzwQ7A==", 874 | "dependencies": [ 875 | "@smithy/types", 876 | "tslib" 877 | ] 878 | }, 879 | "@smithy/protocol-http@5.1.0": { 880 | "integrity": "sha512-KxAOL1nUNw2JTYrtviRRjEnykIDhxc84qMBzxvu1MUfQfHTuBlCG7PA6EdVwqpJjH7glw7FqQoFxUJSyBQgu7g==", 881 | "dependencies": [ 882 | "@smithy/types", 883 | "tslib" 884 | ] 885 | }, 886 | "@smithy/querystring-builder@4.0.2": { 887 | "integrity": "sha512-NTOs0FwHw1vimmQM4ebh+wFQvOwkEf/kQL6bSM1Lock+Bv4I89B3hGYoUEPkmvYPkDKyp5UdXJYu+PoTQ3T31Q==", 888 | "dependencies": [ 889 | "@smithy/types", 890 | "@smithy/util-uri-escape", 891 | "tslib" 892 | ] 893 | }, 894 | "@smithy/querystring-parser@4.0.2": { 895 | "integrity": "sha512-v6w8wnmZcVXjfVLjxw8qF7OwESD9wnpjp0Dqry/Pod0/5vcEA3qxCr+BhbOHlxS8O+29eLpT3aagxXGwIoEk7Q==", 896 | "dependencies": [ 897 | "@smithy/types", 898 | "tslib" 899 | ] 900 | }, 901 | "@smithy/service-error-classification@4.0.2": { 902 | "integrity": "sha512-LA86xeFpTKn270Hbkixqs5n73S+LVM0/VZco8dqd+JT75Dyx3Lcw/MraL7ybjmz786+160K8rPOmhsq0SocoJQ==", 903 | "dependencies": [ 904 | "@smithy/types" 905 | ] 906 | }, 907 | "@smithy/shared-ini-file-loader@4.0.2": { 908 | "integrity": "sha512-J9/gTWBGVuFZ01oVA6vdb4DAjf1XbDhK6sLsu3OS9qmLrS6KB5ygpeHiM3miIbj1qgSJ96GYszXFWv6ErJ8QEw==", 909 | "dependencies": [ 910 | "@smithy/types", 911 | "tslib" 912 | ] 913 | }, 914 | "@smithy/signature-v4@5.0.2": { 915 | "integrity": "sha512-Mz+mc7okA73Lyz8zQKJNyr7lIcHLiPYp0+oiqiMNc/t7/Kf2BENs5d63pEj7oPqdjaum6g0Fc8wC78dY1TgtXw==", 916 | "dependencies": [ 917 | "@smithy/is-array-buffer@4.0.0", 918 | "@smithy/protocol-http", 919 | "@smithy/types", 920 | "@smithy/util-hex-encoding", 921 | "@smithy/util-middleware", 922 | "@smithy/util-uri-escape", 923 | "@smithy/util-utf8@4.0.0", 924 | "tslib" 925 | ] 926 | }, 927 | "@smithy/smithy-client@4.2.0": { 928 | "integrity": "sha512-Qs65/w30pWV7LSFAez9DKy0Koaoh3iHhpcpCCJ4waj/iqwsuSzJna2+vYwq46yBaqO5ZbP9TjUsATUNxrKeBdw==", 929 | "dependencies": [ 930 | "@smithy/core", 931 | "@smithy/middleware-endpoint", 932 | "@smithy/middleware-stack", 933 | "@smithy/protocol-http", 934 | "@smithy/types", 935 | "@smithy/util-stream", 936 | "tslib" 937 | ] 938 | }, 939 | "@smithy/types@4.2.0": { 940 | "integrity": "sha512-7eMk09zQKCO+E/ivsjQv+fDlOupcFUCSC/L2YUPgwhvowVGWbPQHjEFcmjt7QQ4ra5lyowS92SV53Zc6XD4+fg==", 941 | "dependencies": [ 942 | "tslib" 943 | ] 944 | }, 945 | "@smithy/url-parser@4.0.2": { 946 | "integrity": "sha512-Bm8n3j2ScqnT+kJaClSVCMeiSenK6jVAzZCNewsYWuZtnBehEz4r2qP0riZySZVfzB+03XZHJeqfmJDkeeSLiQ==", 947 | "dependencies": [ 948 | "@smithy/querystring-parser", 949 | "@smithy/types", 950 | "tslib" 951 | ] 952 | }, 953 | "@smithy/util-base64@4.0.0": { 954 | "integrity": "sha512-CvHfCmO2mchox9kjrtzoHkWHxjHZzaFojLc8quxXY7WAAMAg43nuxwv95tATVgQFNDwd4M9S1qFzj40Ul41Kmg==", 955 | "dependencies": [ 956 | "@smithy/util-buffer-from@4.0.0", 957 | "@smithy/util-utf8@4.0.0", 958 | "tslib" 959 | ] 960 | }, 961 | "@smithy/util-body-length-browser@4.0.0": { 962 | "integrity": "sha512-sNi3DL0/k64/LO3A256M+m3CDdG6V7WKWHdAiBBMUN8S3hK3aMPhwnPik2A/a2ONN+9doY9UxaLfgqsIRg69QA==", 963 | "dependencies": [ 964 | "tslib" 965 | ] 966 | }, 967 | "@smithy/util-body-length-node@4.0.0": { 968 | "integrity": "sha512-q0iDP3VsZzqJyje8xJWEJCNIu3lktUGVoSy1KB0UWym2CL1siV3artm+u1DFYTLejpsrdGyCSWBdGNjJzfDPjg==", 969 | "dependencies": [ 970 | "tslib" 971 | ] 972 | }, 973 | "@smithy/util-buffer-from@2.2.0": { 974 | "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", 975 | "dependencies": [ 976 | "@smithy/is-array-buffer@2.2.0", 977 | "tslib" 978 | ] 979 | }, 980 | "@smithy/util-buffer-from@4.0.0": { 981 | "integrity": "sha512-9TOQ7781sZvddgO8nxueKi3+yGvkY35kotA0Y6BWRajAv8jjmigQ1sBwz0UX47pQMYXJPahSKEKYFgt+rXdcug==", 982 | "dependencies": [ 983 | "@smithy/is-array-buffer@4.0.0", 984 | "tslib" 985 | ] 986 | }, 987 | "@smithy/util-config-provider@4.0.0": { 988 | "integrity": "sha512-L1RBVzLyfE8OXH+1hsJ8p+acNUSirQnWQ6/EgpchV88G6zGBTDPdXiiExei6Z1wR2RxYvxY/XLw6AMNCCt8H3w==", 989 | "dependencies": [ 990 | "tslib" 991 | ] 992 | }, 993 | "@smithy/util-defaults-mode-browser@4.0.8": { 994 | "integrity": "sha512-ZTypzBra+lI/LfTYZeop9UjoJhhGRTg3pxrNpfSTQLd3AJ37r2z4AXTKpq1rFXiiUIJsYyFgNJdjWRGP/cbBaQ==", 995 | "dependencies": [ 996 | "@smithy/property-provider", 997 | "@smithy/smithy-client", 998 | "@smithy/types", 999 | "bowser", 1000 | "tslib" 1001 | ] 1002 | }, 1003 | "@smithy/util-defaults-mode-node@4.0.8": { 1004 | "integrity": "sha512-Rgk0Jc/UDfRTzVthye/k2dDsz5Xxs9LZaKCNPgJTRyoyBoeiNCnHsYGOyu1PKN+sDyPnJzMOz22JbwxzBp9NNA==", 1005 | "dependencies": [ 1006 | "@smithy/config-resolver", 1007 | "@smithy/credential-provider-imds", 1008 | "@smithy/node-config-provider", 1009 | "@smithy/property-provider", 1010 | "@smithy/smithy-client", 1011 | "@smithy/types", 1012 | "tslib" 1013 | ] 1014 | }, 1015 | "@smithy/util-endpoints@3.0.2": { 1016 | "integrity": "sha512-6QSutU5ZyrpNbnd51zRTL7goojlcnuOB55+F9VBD+j8JpRY50IGamsjlycrmpn8PQkmJucFW8A0LSfXj7jjtLQ==", 1017 | "dependencies": [ 1018 | "@smithy/node-config-provider", 1019 | "@smithy/types", 1020 | "tslib" 1021 | ] 1022 | }, 1023 | "@smithy/util-hex-encoding@4.0.0": { 1024 | "integrity": "sha512-Yk5mLhHtfIgW2W2WQZWSg5kuMZCVbvhFmC7rV4IO2QqnZdbEFPmQnCcGMAX2z/8Qj3B9hYYNjZOhWym+RwhePw==", 1025 | "dependencies": [ 1026 | "tslib" 1027 | ] 1028 | }, 1029 | "@smithy/util-middleware@4.0.2": { 1030 | "integrity": "sha512-6GDamTGLuBQVAEuQ4yDQ+ti/YINf/MEmIegrEeg7DdB/sld8BX1lqt9RRuIcABOhAGTA50bRbPzErez7SlDtDQ==", 1031 | "dependencies": [ 1032 | "@smithy/types", 1033 | "tslib" 1034 | ] 1035 | }, 1036 | "@smithy/util-retry@4.0.2": { 1037 | "integrity": "sha512-Qryc+QG+7BCpvjloFLQrmlSd0RsVRHejRXd78jNO3+oREueCjwG1CCEH1vduw/ZkM1U9TztwIKVIi3+8MJScGg==", 1038 | "dependencies": [ 1039 | "@smithy/service-error-classification", 1040 | "@smithy/types", 1041 | "tslib" 1042 | ] 1043 | }, 1044 | "@smithy/util-stream@4.2.0": { 1045 | "integrity": "sha512-Vj1TtwWnuWqdgQI6YTUF5hQ/0jmFiOYsc51CSMgj7QfyO+RF4EnT2HNjoviNlOOmgzgvf3f5yno+EiC4vrnaWQ==", 1046 | "dependencies": [ 1047 | "@smithy/fetch-http-handler", 1048 | "@smithy/node-http-handler", 1049 | "@smithy/types", 1050 | "@smithy/util-base64", 1051 | "@smithy/util-buffer-from@4.0.0", 1052 | "@smithy/util-hex-encoding", 1053 | "@smithy/util-utf8@4.0.0", 1054 | "tslib" 1055 | ] 1056 | }, 1057 | "@smithy/util-uri-escape@4.0.0": { 1058 | "integrity": "sha512-77yfbCbQMtgtTylO9itEAdpPXSog3ZxMe09AEhm0dU0NLTalV70ghDZFR+Nfi1C60jnJoh/Re4090/DuZh2Omg==", 1059 | "dependencies": [ 1060 | "tslib" 1061 | ] 1062 | }, 1063 | "@smithy/util-utf8@2.3.0": { 1064 | "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", 1065 | "dependencies": [ 1066 | "@smithy/util-buffer-from@2.2.0", 1067 | "tslib" 1068 | ] 1069 | }, 1070 | "@smithy/util-utf8@4.0.0": { 1071 | "integrity": "sha512-b+zebfKCfRdgNJDknHCob3O7FpeYQN6ZG6YLExMcasDHsCXlsXCEuiPZeLnJLpwa5dvPetGlnGCiMHuLwGvFow==", 1072 | "dependencies": [ 1073 | "@smithy/util-buffer-from@4.0.0", 1074 | "tslib" 1075 | ] 1076 | }, 1077 | "@smithy/util-waiter@4.0.3": { 1078 | "integrity": "sha512-JtaY3FxmD+te+KSI2FJuEcfNC9T/DGGVf551babM7fAaXhjJUt7oSYurH1Devxd2+BOSUACCgt3buinx4UnmEA==", 1079 | "dependencies": [ 1080 | "@smithy/abort-controller", 1081 | "@smithy/types", 1082 | "tslib" 1083 | ] 1084 | }, 1085 | "bowser@2.11.0": { 1086 | "integrity": "sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA==" 1087 | }, 1088 | "fast-xml-parser@4.4.1": { 1089 | "integrity": "sha512-xkjOecfnKGkSsOwtZ5Pz7Us/T6mrbPQrq0nh+aCO5V9nk5NLWmasAHumTKjiPJPWANe+kAZ84Jc8ooJkzZ88Sw==", 1090 | "dependencies": [ 1091 | "strnum" 1092 | ], 1093 | "bin": true 1094 | }, 1095 | "hono@4.7.5": { 1096 | "integrity": "sha512-fDOK5W2C1vZACsgLONigdZTRZxuBqFtcKh7bUQ5cVSbwI2RWjloJDcgFOVzbQrlI6pCmhlTsVYZ7zpLj4m4qMQ==" 1097 | }, 1098 | "strnum@1.1.2": { 1099 | "integrity": "sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA==" 1100 | }, 1101 | "tslib@2.8.1": { 1102 | "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" 1103 | }, 1104 | "uuid@9.0.1": { 1105 | "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", 1106 | "bin": true 1107 | } 1108 | }, 1109 | "workspace": { 1110 | "dependencies": [ 1111 | "jsr:@david/dax@0.43", 1112 | "jsr:@std/assert@^1.0.13", 1113 | "jsr:@std/path@^1.0.9", 1114 | "jsr:@std/testing@^1.0.11", 1115 | "npm:@aws-sdk/client-s3@^3.779.0", 1116 | "npm:@aws-sdk/s3-request-presigner@^3.779.0", 1117 | "npm:hono@^4.7.5" 1118 | ] 1119 | } 1120 | } 1121 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | s3: 3 | image: minio/minio:RELEASE.2025-04-08T15-41-24Z 4 | entrypoint: sh -c "minio server /data --console-address ':9001'" 5 | environment: 6 | MINIO_ROOT_USER: minio 7 | MINIO_ROOT_PASSWORD: minio123 8 | MINIO_DOMAIN: localhost 9 | ports: 10 | - 9000:9000 11 | - 9001:9001 12 | volumes: 13 | - s3_data:/data 14 | healthcheck: 15 | test: ['CMD', 'curl', '-f', 'http://localhost:9000/minio/health/live'] 16 | interval: 10s 17 | timeout: 5s 18 | retries: 5 19 | start_period: 5s 20 | create_bucket_and_user: 21 | image: minio/mc:RELEASE.2025-04-08T15-39-49Z 22 | entrypoint: sh -c "mc config host add minio http://s3:9000 minio minio123 && mc mb minio/nx-cloud && mc anonymous set public minio/nx-cloud" 23 | depends_on: 24 | s3: 25 | condition: service_healthy 26 | environment: 27 | MINIO_ACCESS_KEY: minio 28 | MINIO_SECRET_KEY: minio123 29 | 30 | cache_server: 31 | build: 32 | context: . 33 | dockerfile: Dockerfile 34 | depends_on: 35 | create_bucket_and_user: 36 | condition: service_completed_successfully 37 | ports: 38 | - 3000:3000 39 | environment: 40 | - S3_ENDPOINT_URL=http://s3:9000 41 | env_file: 42 | - .env.local 43 | 44 | volumes: 45 | s3_data: 46 | -------------------------------------------------------------------------------- /e2e/e2e.test.ts: -------------------------------------------------------------------------------- 1 | import $ from '@david/dax'; 2 | import { beforeAll, describe, it } from '@std/testing/bdd'; 3 | import { join } from '@std/path/join'; 4 | 5 | function generateRandomString(length: number) { 6 | return Math.random().toString(36).substring(2, 2 + length); 7 | } 8 | 9 | describe('Remote Cache', () => { 10 | const workspaceName = generateRandomString(10); 11 | 12 | beforeAll(async () => { 13 | await $`rm -rf tmp`; 14 | await $`mkdir -p tmp`; 15 | 16 | await $`npx -y create-nx-workspace@20.8 --name=${workspaceName} --preset=react-monorepo --interactive=false --workspaceType=integrated --appName=web --e2eTestRunner=none --unitTestRunner=none --skipGit` 17 | .cwd(join(Deno.cwd(), 'tmp')); 18 | }); 19 | 20 | it('should store and retrieve cache artifacts', async () => { 21 | const workspacePath = join(Deno.cwd(), 'tmp', workspaceName); 22 | 23 | try { 24 | // Configure Nx to use our cache server 25 | await $`echo 'NX_SELF_HOSTED_REMOTE_CACHE_SERVER=http://localhost:3000' >> .env` 26 | .cwd(workspacePath); 27 | await $`echo 'NX_SELF_HOSTED_REMOTE_CACHE_ACCESS_TOKEN=test-token' >> .env` 28 | .cwd(workspacePath); 29 | 30 | // First build - should miss cache 31 | const firstBuild = await $`npx nx build web --verbose` 32 | .cwd(workspacePath) 33 | .env('NX_SELF_HOSTED_REMOTE_CACHE_SERVER', 'http://localhost:3000') 34 | .env('NX_SELF_HOSTED_REMOTE_CACHE_ACCESS_TOKEN', 'test-token') 35 | .printCommand().stdout('inheritPiped'); 36 | 37 | // Verify cache miss 38 | if ( 39 | !firstBuild.stdout.includes( 40 | 'Successfully ran target build for project web', 41 | ) 42 | ) { 43 | console.log(firstBuild.stdout); 44 | throw new Error('Expected cache miss on first build'); 45 | } 46 | 47 | // Second build - should hit cache 48 | const secondBuild = await $`npx nx build web` 49 | .cwd(workspacePath) 50 | .env('NX_SELF_HOSTED_REMOTE_CACHE_SERVER', 'http://localhost:3000') 51 | .env('NX_SELF_HOSTED_REMOTE_CACHE_ACCESS_TOKEN', 'test-token') 52 | .printCommand().stdout('inheritPiped'); 53 | 54 | // Verify cache hit 55 | if ( 56 | !secondBuild.stdout.includes( 57 | 'Nx read the output from the cache instead', 58 | ) 59 | ) { 60 | console.log(secondBuild.stdout); 61 | throw new Error('Expected cache hit on second build'); 62 | } 63 | } catch (error) { 64 | throw error; 65 | } 66 | }); 67 | }); 68 | -------------------------------------------------------------------------------- /scripts/wait-for-bucket.ts: -------------------------------------------------------------------------------- 1 | import { HeadBucketCommand, S3Client } from 'npm:@aws-sdk/client-s3'; 2 | 3 | const BUCKET = Deno.env.get('S3_BUCKET_NAME')!; 4 | 5 | const s3 = new S3Client({ 6 | region: Deno.env.get('AWS_REGION')!, 7 | endpoint: Deno.env.get('S3_ENDPOINT_URL')!, 8 | credentials: { 9 | accessKeyId: Deno.env.get('AWS_ACCESS_KEY_ID')!, 10 | secretAccessKey: Deno.env.get('AWS_SECRET_ACCESS_KEY')!, 11 | }, 12 | forcePathStyle: true, 13 | }); 14 | 15 | let found = false; 16 | for (let i = 0; i < 30; i++) { 17 | try { 18 | await s3.send(new HeadBucketCommand({ Bucket: BUCKET })); 19 | found = true; 20 | break; 21 | } catch (error) { 22 | console.error('Error waiting for bucket', error); 23 | await new Promise((r) => setTimeout(r, 2000)); 24 | } 25 | } 26 | if (!found) { 27 | console.error('Bucket not found after waiting'); 28 | Deno.exit(1); 29 | } 30 | console.log('Bucket is ready!'); 31 | -------------------------------------------------------------------------------- /src/index.test.ts: -------------------------------------------------------------------------------- 1 | import { assertEquals, assertExists } from '@std/assert'; 2 | import { app } from './index.ts'; 3 | 4 | async function makeRequest( 5 | method: string, 6 | path: string, 7 | headers: Record = {}, 8 | body?: Uint8Array, 9 | ) { 10 | const req = new Request(`http://localhost${path}`, { 11 | method, 12 | headers: { 13 | 'Authorization': 'Bearer test-token', 14 | ...headers, 15 | }, 16 | body, 17 | }); 18 | 19 | return await app.fetch(req, { 20 | NX_CACHE_ACCESS_TOKEN: 'test-token', 21 | AWS_REGION: 'us-east-1', 22 | AWS_ACCESS_KEY_ID: 'minio', 23 | AWS_SECRET_ACCESS_KEY: 'minio123', 24 | S3_BUCKET_NAME: 'nx-cloud', 25 | S3_ENDPOINT_URL: 'http://localhost:9000', 26 | }); 27 | } 28 | 29 | Deno.test('PUT /v1/cache/{hash} - Success', async () => { 30 | const hash = crypto.randomUUID(); 31 | 32 | const response = await makeRequest( 33 | 'PUT', 34 | `/v1/cache/${hash}`, 35 | { 'Content-Type': 'application/octet-stream' }, 36 | Deno.readFileSync('./src/index.ts'), 37 | ); 38 | 39 | assertEquals(response.status, 202); 40 | const body = await response.text(); 41 | assertEquals(body, 'Successfully uploaded'); 42 | }); 43 | 44 | Deno.test('PUT /v1/cache/{hash} - Unauthorized', async () => { 45 | const hash = crypto.randomUUID(); 46 | 47 | const response = await makeRequest( 48 | 'PUT', 49 | `/v1/cache/${hash}`, 50 | { 51 | 'Authorization': 'Bearer wrong-token', 52 | 'Content-Length': '10', 53 | }, 54 | Deno.readFileSync('./src/index.ts'), 55 | ); 56 | 57 | assertEquals(response.status, 403); 58 | const body = await response.text(); 59 | assertEquals(body, 'Access forbidden'); 60 | }); 61 | 62 | Deno.test('GET /v1/cache/{hash} - Success', async () => { 63 | const hash = crypto.randomUUID(); 64 | 65 | await makeRequest('PUT', `/v1/cache/${hash}`, { 66 | 'Content-Length': '10', 67 | }, Deno.readFileSync('./src/index.ts')); 68 | 69 | const response = await makeRequest('GET', `/v1/cache/${hash}`); 70 | 71 | assertEquals(response.status, 200); 72 | assertExists(response.headers.get('content-type')); 73 | 74 | const body = await response.text(); 75 | assertEquals(body, Deno.readTextFileSync('./src/index.ts')); 76 | }); 77 | 78 | Deno.test('GET /v1/cache/{hash} - Unauthorized', async () => { 79 | const hash = crypto.randomUUID(); 80 | 81 | const response = await makeRequest( 82 | 'GET', 83 | `/v1/cache/${hash}`, 84 | { 'Authorization': 'Bearer wrong-token' }, 85 | ); 86 | 87 | assertEquals(response.status, 403); 88 | const body = await response.text(); 89 | assertEquals(body, 'Access forbidden'); 90 | }); 91 | 92 | Deno.test('GET /v1/cache/{hash} - Not Found', async () => { 93 | const hash = crypto.randomUUID(); 94 | 95 | const response = await makeRequest('GET', `/v1/cache/${hash}`); 96 | 97 | assertEquals(response.status, 404); 98 | const body = await response.text(); 99 | assertEquals(body, 'The record was not found'); 100 | }); 101 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { Hono } from 'hono'; 2 | import { createMiddleware } from 'hono/factory'; 3 | import { logger } from 'hono/logger'; 4 | 5 | import { 6 | GetObjectCommand, 7 | HeadObjectCommand, 8 | PutObjectCommand, 9 | S3Client, 10 | } from '@aws-sdk/client-s3'; 11 | import { getSignedUrl } from '@aws-sdk/s3-request-presigner'; 12 | 13 | export const app = new Hono<{ 14 | Bindings: { 15 | NX_CACHE_ACCESS_TOKEN: string; 16 | AWS_REGION: string; 17 | AWS_ACCESS_KEY_ID: string; 18 | AWS_SECRET_ACCESS_KEY: string; 19 | S3_BUCKET_NAME: string; 20 | S3_ENDPOINT_URL: string; 21 | }; 22 | Variables: { 23 | s3: S3Client; 24 | }; 25 | }>(); 26 | 27 | app.use(async (c, next) => { 28 | c.set( 29 | 's3', 30 | new S3Client({ 31 | region: c.env.AWS_REGION, 32 | endpoint: c.env.S3_ENDPOINT_URL, 33 | credentials: { 34 | accessKeyId: c.env.AWS_ACCESS_KEY_ID, 35 | secretAccessKey: c.env.AWS_SECRET_ACCESS_KEY, 36 | }, 37 | forcePathStyle: true, 38 | }), 39 | ); 40 | 41 | await next(); 42 | }); 43 | 44 | const auth = () => 45 | createMiddleware(async (c, next) => { 46 | const authHeader = c.req.header('Authorization'); 47 | if (!authHeader || !authHeader.startsWith('Bearer ')) { 48 | return new Response('Unauthorized', { 49 | status: 401, 50 | headers: { 'Content-Type': 'text/plain' }, 51 | }); 52 | } 53 | 54 | const token = authHeader.split(' ')[1]; 55 | 56 | if (token !== c.env.NX_CACHE_ACCESS_TOKEN) { 57 | return new Response('Access forbidden', { 58 | status: 403, 59 | headers: { 'Content-Type': 'text/plain' }, 60 | }); 61 | } 62 | 63 | await next(); 64 | }); 65 | 66 | app.use(logger()); 67 | 68 | app.get('/health', () => { 69 | return new Response('OK', { 70 | status: 200, 71 | headers: { 'Content-Type': 'text/plain' }, 72 | }); 73 | }); 74 | 75 | app.put('/v1/cache/:hash', auth(), async (c) => { 76 | try { 77 | const hash = c.req.param('hash'); 78 | 79 | try { 80 | await c.get('s3').send( 81 | new HeadObjectCommand({ 82 | Bucket: c.env.S3_BUCKET_NAME, 83 | Key: hash, 84 | }), 85 | ); 86 | 87 | return new Response('Cannot override an existing record', { 88 | status: 409, 89 | headers: { 'Content-Type': 'text/plain' }, 90 | }); 91 | } catch (error: unknown) { 92 | if (error instanceof Error && error.name === 'NotFound') { 93 | // Do nothing 94 | } else { 95 | console.error('Upload error:', error); 96 | return new Response('Internal server error', { 97 | status: 500, 98 | headers: { 'Content-Type': 'text/plain' }, 99 | }); 100 | } 101 | } 102 | 103 | const body = await c.req.arrayBuffer(); 104 | 105 | await c.get('s3').send( 106 | new PutObjectCommand({ 107 | Bucket: c.env.S3_BUCKET_NAME, 108 | Key: hash, 109 | Body: new Uint8Array(body), 110 | }), 111 | ); 112 | 113 | return new Response('Successfully uploaded', { 114 | status: 202, 115 | headers: { 'Content-Type': 'text/plain' }, 116 | }); 117 | } catch (error: unknown) { 118 | console.error('Upload error:', error); 119 | return new Response('Internal server error', { 120 | status: 500, 121 | headers: { 'Content-Type': 'text/plain' }, 122 | }); 123 | } 124 | }); 125 | 126 | app.get('/v1/cache/:hash', auth(), async (c) => { 127 | try { 128 | const hash = c.req.param('hash'); 129 | 130 | const command = new GetObjectCommand({ 131 | Bucket: c.env.S3_BUCKET_NAME, 132 | Key: hash, 133 | }); 134 | 135 | const url = await getSignedUrl(c.get('s3'), command, { 136 | expiresIn: 18000, 137 | }); 138 | 139 | const response = await fetch(url); 140 | 141 | if (!response.ok) { 142 | console.error('Download error:', response.statusText); 143 | 144 | await response.body?.cancel(); 145 | 146 | if (response.status === 404) { 147 | return new Response('The record was not found', { 148 | status: 404, 149 | headers: { 'Content-Type': 'text/plain' }, 150 | }); 151 | } 152 | 153 | return new Response('Access forbidden', { 154 | status: 403, 155 | headers: { 'Content-Type': 'text/plain' }, 156 | }); 157 | } 158 | 159 | return response; 160 | } catch (error: unknown) { 161 | if (error instanceof Error && error.name === 'NoSuchKey') { 162 | return new Response('The record was not found', { 163 | status: 404, 164 | headers: { 'Content-Type': 'text/plain' }, 165 | }); 166 | } 167 | console.error('Download error:', error); 168 | return new Response('Internal server error', { 169 | status: 500, 170 | headers: { 'Content-Type': 'text/plain' }, 171 | }); 172 | } 173 | }); 174 | 175 | if (import.meta.main) { 176 | const port = parseInt(Deno.env.get('PORT') || '3000'); 177 | console.log(`Server running on port ${port}`); 178 | 179 | Deno.serve({ port }, (req) => 180 | app.fetch(req, { 181 | NX_CACHE_ACCESS_TOKEN: Deno.env.get('NX_CACHE_ACCESS_TOKEN'), 182 | AWS_REGION: Deno.env.get('AWS_REGION') || 'us-east-1', 183 | AWS_ACCESS_KEY_ID: Deno.env.get('AWS_ACCESS_KEY_ID'), 184 | AWS_SECRET_ACCESS_KEY: Deno.env.get('AWS_SECRET_ACCESS_KEY'), 185 | S3_BUCKET_NAME: Deno.env.get('S3_BUCKET_NAME') || 'nx-cloud', 186 | S3_ENDPOINT_URL: Deno.env.get('S3_ENDPOINT_URL'), 187 | })); 188 | } 189 | --------------------------------------------------------------------------------