diff --git a/.dockerignore b/.dockerignore deleted file mode 100644 index 6992388..0000000 --- a/.dockerignore +++ /dev/null @@ -1,53 +0,0 @@ -# Rust build artifacts -target/ -**/*.rs.bk -Cargo.lock - -# Git and version control -.git/ -.gitignore - -# IDE and editor files -.vscode/ -.idea/ -*.swp -*.swo -*~ - -# OS generated files -.DS_Store -.DS_Store? -._* -.Spotlight-V100 -.Trashes -ehthumbs.db -Thumbs.db - -# Documentation and scripts -docs/ -*.md -scripts/ -deploy/ - -# CI/CD -.github/ -.github/workflows/ - -# AWS CDK -cdk.out/ -*.d.ts -*.js.map - -# Node modules (if any) -node_modules/ -npm-debug.log* -yarn-debug.log* -yarn-error.log* - -# Logs -*.log - -# Environment files -.env -.env.local -.env.*.local diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml deleted file mode 100644 index 0201f86..0000000 --- a/.github/workflows/ci.yaml +++ /dev/null @@ -1,19 +0,0 @@ -name: CI - -on: - push: - -jobs: - test: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - - name: Set up Rust - run: rustup show - - name: Format - run: cargo fmt --check - - name: Clippy - run: cargo clippy -- -D warnings - - name: Run tests - run: cargo test diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..da90937 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,110 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +env: + CARGO_TERM_COLOR: always + +jobs: + fmt: + name: Format + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt + - run: cargo fmt --all -- --check + + clippy: + name: Clippy + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + with: + components: clippy + targets: wasm32-unknown-unknown + - uses: Swatinem/rust-cache@v2 + - run: cargo clippy --target wasm32-unknown-unknown -- -D warnings + + test: + name: Unit Tests + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + - run: cargo test + + check: + name: Check + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + with: + targets: wasm32-unknown-unknown + - uses: Swatinem/rust-cache@v2 + - run: cargo check --target wasm32-unknown-unknown + + build: + name: Build Worker + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + with: + targets: wasm32-unknown-unknown + - uses: Swatinem/rust-cache@v2 + - name: Install worker-build + run: cargo install worker-build@0.7.5 + - name: Build WASM worker + run: worker-build --release + + integration: + name: Integration Tests + runs-on: ubuntu-latest + needs: [build] + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + with: + targets: wasm32-unknown-unknown + - uses: Swatinem/rust-cache@v2 + - uses: actions/setup-node@v4 + with: + node-version: "22" + - uses: astral-sh/setup-uv@v5 + - name: Install wrangler + run: npm install -g wrangler@3 + - name: Install worker-build + run: cargo install worker-build@0.7.5 + - name: Start wrangler dev + run: | + wrangler dev --port 8787 & + for i in $(seq 1 60); do + if curl -s --max-time 2 http://localhost:8787/ > /dev/null 2>&1; then + echo "Server ready after ${i}s" + break + fi + sleep 1 + done + - name: Run integration tests + run: uvx --with requests pytest tests/ -v + + audit: + name: Security Audit + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - name: Install cargo-audit + uses: taiki-e/install-action@v2 + with: + tool: cargo-audit + - run: cargo audit diff --git a/.github/workflows/conventional-commits.yaml b/.github/workflows/conventional-commits.yaml index 1ffe074..0c76134 100644 --- a/.github/workflows/conventional-commits.yaml +++ b/.github/workflows/conventional-commits.yaml @@ -11,8 +11,8 @@ jobs: - uses: actions/create-github-app-token@5d869da34e18e7287c1daad50e0b8ea0f506ce69 # v 1.11.0 id: app-token with: - app-id: ${{ vars.APP_ID }} - private-key: ${{ secrets.PRIVATE_KEY }} + app-id: ${{ vars.SC_RELEASE_BOT_ID }} + private-key: ${{ secrets.SC_RELEASE_BOT_PRIVATE_KEY }} - name: PR Conventional Commit Validation uses: ytanikin/pr-conventional-commits@1.4.0 diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..3fc437d --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,79 @@ +name: Deploy + +on: + workflow_call: + inputs: + worker_name: + description: "Cloudflare Worker name override (cannot be used with wrangler_env)" + required: false + type: string + default: "" + wrangler_env: + description: "Wrangler environment (e.g. staging). Cannot be used with worker_name." + required: false + type: string + default: "" + wrangler_config: + description: "Wrangler config file (default: wrangler.toml)" + required: false + type: string + default: "wrangler.toml" + var_overrides: + description: "Additional --var flags (e.g. 'LOG_LEVEL:DEBUG SOURCE_API_URL:https://staging.source.coop')" + required: false + type: string + default: "" + environment: + description: "GitHub environment name (preview, staging, production)" + required: true + type: string + outputs: + deploy_url: + description: "The deployed worker URL" + value: ${{ jobs.deploy.outputs.deploy_url }} + secrets: + CLOUDFLARE_API_TOKEN: + required: true + CLOUDFLARE_ACCOUNT_ID: + required: true + +jobs: + deploy: + name: Deploy + runs-on: ubuntu-latest + environment: ${{ inputs.environment }} + outputs: + deploy_url: ${{ steps.url.outputs.deploy_url }} + env: + CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} + CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + with: + targets: wasm32-unknown-unknown + - uses: Swatinem/rust-cache@v2 + - uses: actions/setup-node@v4 + with: + node-version: "22" + + - name: Deploy worker + run: | + EXTRA_ARGS="" + if [ -n "${{ inputs.wrangler_env }}" ]; then + EXTRA_ARGS="--env ${{ inputs.wrangler_env }}" + fi + if [ -n "${{ inputs.worker_name }}" ]; then + EXTRA_ARGS="--name ${{ inputs.worker_name }}" + fi + for var in ${{ inputs.var_overrides }}; do + EXTRA_ARGS="$EXTRA_ARGS --var $var" + done + npx wrangler@3 deploy --config ${{ inputs.wrangler_config }} $EXTRA_ARGS + + - name: Get deploy URL + id: url + run: | + WORKER="${{ inputs.worker_name || 'source-data-proxy' }}" + URL="https://${WORKER}.${{ vars.CLOUDFLARE_WORKERS_SUBDOMAIN }}.workers.dev" + echo "deploy_url=$URL" >> "$GITHUB_OUTPUT" diff --git a/.github/workflows/please-release.yaml b/.github/workflows/please-release.yaml index 162efa9..01dbab9 100644 --- a/.github/workflows/please-release.yaml +++ b/.github/workflows/please-release.yaml @@ -16,8 +16,8 @@ jobs: - uses: actions/create-github-app-token@5d869da34e18e7287c1daad50e0b8ea0f506ce69 # v 1.11.0 id: app-token with: - app-id: ${{ vars.APP_ID }} - private-key: ${{ secrets.PRIVATE_KEY }} + app-id: ${{ vars.SC_RELEASE_BOT_ID }} + private-key: ${{ secrets.SC_RELEASE_BOT_PRIVATE_KEY }} - uses: googleapis/release-please-action@078b9b8dda7799db29972b379561ff8e19b08e82 # v4 with: diff --git a/.github/workflows/preview.yml b/.github/workflows/preview.yml new file mode 100644 index 0000000..67639ea --- /dev/null +++ b/.github/workflows/preview.yml @@ -0,0 +1,62 @@ +name: Preview + +on: + pull_request: + types: [opened, synchronize, reopened, closed] + +concurrency: + group: preview-${{ github.event.pull_request.number }} + cancel-in-progress: true + +jobs: + deploy: + name: Deploy & Test + if: github.event.action != 'closed' + uses: ./.github/workflows/deploy.yml + with: + worker_name: source-data-proxy-pr-${{ github.event.pull_request.number }} + wrangler_config: wrangler.preview.toml + var_overrides: "LOG_LEVEL:DEBUG SOURCE_API_URL:https://staging.source.coop" + environment: preview + secrets: + CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} + CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + + comment: + name: PR Comment + needs: deploy + if: github.event.action != 'closed' + runs-on: ubuntu-latest + permissions: + pull-requests: write + steps: + - name: Find Comment + uses: peter-evans/find-comment@v4 + id: find-comment + with: + issue-number: ${{ github.event.pull_request.number }} + comment-author: "github-actions[bot]" + body-includes: Latest commit deployed to + + - name: Create or update comment + uses: peter-evans/create-or-update-comment@v5 + with: + comment-id: ${{ steps.find-comment.outputs.comment-id }} + issue-number: ${{ github.event.pull_request.number }} + body: | + 🚀 Latest commit deployed to ${{ needs.deploy.outputs.deploy_url }} + + * Date: `${{ github.event.pull_request.updated_at }}` + * Commit: ${{ github.sha }} + edit-mode: replace + + cleanup: + name: Cleanup Preview + if: github.event.action == 'closed' + runs-on: ubuntu-latest + env: + CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} + CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + steps: + - name: Delete preview worker + run: npx wrangler@3 delete --name "source-data-proxy-pr-${{ github.event.pull_request.number }}" --force diff --git a/.github/workflows/prod-deploy.yaml b/.github/workflows/prod-deploy.yaml deleted file mode 100644 index 58f88fd..0000000 --- a/.github/workflows/prod-deploy.yaml +++ /dev/null @@ -1,46 +0,0 @@ -name: Deploy to Prod - -on: - release: - types: [published, deleted] - workflow_dispatch: - -permissions: - id-token: write - contents: read - -concurrency: - group: production - cancel-in-progress: false - -jobs: - deploy-cdk: - runs-on: ubuntu-latest - environment: production - env: - STAGE: prod - defaults: - run: - working-directory: ./deploy - steps: - - uses: actions/checkout@v4.2.2 - - name: Configure AWS Credentials - uses: aws-actions/configure-aws-credentials@v4 - with: - aws-region: ${{ vars.AWS_REGION }} - role-to-assume: ${{ vars.DEPLOY_ROLE_ARN }} - - uses: actions/setup-node@v4.4.0 - with: - node-version: 22 - cache: npm - cache-dependency-path: ./deploy/package-lock.json - - run: npm install -g aws-cdk - - run: npm ci - - name: Set vars to env - env: - VARS_JSON: ${{ toJSON(vars) }} - run: | - echo "$VARS_JSON" | jq -r 'keys[] as $k | "\($k)=\(.[$k])"' >> $GITHUB_ENV - - run: cdk deploy --all --require-approval never - env: - STAGE: prod diff --git a/.github/workflows/production.yml b/.github/workflows/production.yml new file mode 100644 index 0000000..a256271 --- /dev/null +++ b/.github/workflows/production.yml @@ -0,0 +1,20 @@ +name: Production + +on: + release: + types: [published] + +concurrency: + group: production + cancel-in-progress: true + +jobs: + deploy: + name: Deploy & Test + uses: ./.github/workflows/deploy.yml + with: + worker_name: source-data-proxy + environment: production + secrets: + CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} + CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} diff --git a/.github/workflows/staging-deploy.yaml b/.github/workflows/staging-deploy.yaml deleted file mode 100644 index 92b32f3..0000000 --- a/.github/workflows/staging-deploy.yaml +++ /dev/null @@ -1,45 +0,0 @@ -name: Deploy to Staging - -on: - push: - branches: - - main - workflow_dispatch: - -permissions: - id-token: write - contents: read - -concurrency: - group: staging - cancel-in-progress: false - -jobs: - deploy-cdk: - runs-on: ubuntu-latest - environment: staging - env: - STAGE: dev # in AWS, we call this environment "dev", but outside of that (vercel, github, URLs) it's called "staging" - defaults: - run: - working-directory: ./deploy - steps: - - uses: actions/checkout@v4.2.2 - - name: Configure AWS Credentials - uses: aws-actions/configure-aws-credentials@v4 - with: - aws-region: ${{ vars.AWS_REGION }} - role-to-assume: ${{ vars.DEPLOY_ROLE_ARN }} - - uses: actions/setup-node@v4.4.0 - with: - node-version: 22 - cache: npm - cache-dependency-path: ./deploy/package-lock.json - - run: npm install -g aws-cdk - - run: npm ci - - name: Set vars to env - env: - VARS_JSON: ${{ toJSON(vars) }} - run: | - echo "$VARS_JSON" | jq -r 'keys[] as $k | "\($k)=\(.[$k])"' >> $GITHUB_ENV - - run: cdk deploy --all --require-approval never diff --git a/.github/workflows/staging.yml b/.github/workflows/staging.yml new file mode 100644 index 0000000..1517cf3 --- /dev/null +++ b/.github/workflows/staging.yml @@ -0,0 +1,20 @@ +name: Staging + +on: + push: + branches: [main] + +concurrency: + group: staging + cancel-in-progress: true + +jobs: + deploy: + name: Deploy & Test + uses: ./.github/workflows/deploy.yml + with: + wrangler_env: staging + environment: staging + secrets: + CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} + CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} diff --git a/.gitignore b/.gitignore index 96fa024..6825ac5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ /target +/build .DS_Store -scripts/task_definition.json +.wrangler +node_modules +docs/plans diff --git a/Cargo.lock b/Cargo.lock index 73036bf..581b797 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,268 +2,6 @@ # It is not intended for manual editing. version = 4 -[[package]] -name = "RustyXML" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b5ace29ee3216de37c0546865ad08edef58b0f9e76838ed8959a84a990e58c5" - -[[package]] -name = "actix-codec" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f7b0a21988c1bf877cf4759ef5ddaac04c1c9fe808c9142ecb78ba97d97a28a" -dependencies = [ - "bitflags 2.6.0", - "bytes", - "futures-core", - "futures-sink", - "memchr", - "pin-project-lite", - "tokio", - "tokio-util", - "tracing", -] - -[[package]] -name = "actix-cors" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9e772b3bcafe335042b5db010ab7c09013dad6eac4915c91d8d50902769f331" -dependencies = [ - "actix-utils", - "actix-web", - "derive_more", - "futures-util", - "log", - "once_cell", - "smallvec", -] - -[[package]] -name = "actix-http" -version = "3.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d48f96fc3003717aeb9856ca3d02a8c7de502667ad76eeacd830b48d2e91fac4" -dependencies = [ - "actix-codec", - "actix-rt", - "actix-service", - "actix-tls", - "actix-utils", - "ahash", - "base64 0.22.1", - "bitflags 2.6.0", - "bytes", - "bytestring", - "derive_more", - "encoding_rs", - "futures-core", - "h2", - "http 0.2.12", - "httparse", - "httpdate", - "itoa", - "language-tags", - "local-channel", - "mime", - "percent-encoding", - "pin-project-lite", - "rand 0.8.5", - "sha1", - "smallvec", - "tokio", - "tokio-util", - "tracing", -] - -[[package]] -name = "actix-macros" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb" -dependencies = [ - "quote", - "syn", -] - -[[package]] -name = "actix-router" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13d324164c51f63867b57e73ba5936ea151b8a41a1d23d1031eeb9f70d0236f8" -dependencies = [ - "bytestring", - "cfg-if", - "http 0.2.12", - "regex-lite", - "serde", - "tracing", -] - -[[package]] -name = "actix-rt" -version = "2.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24eda4e2a6e042aa4e55ac438a2ae052d3b5da0ecf83d7411e1a368946925208" -dependencies = [ - "futures-core", - "tokio", -] - -[[package]] -name = "actix-server" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ca2549781d8dd6d75c40cf6b6051260a2cc2f3c62343d761a969a0640646894" -dependencies = [ - "actix-rt", - "actix-service", - "actix-utils", - "futures-core", - "futures-util", - "mio", - "socket2", - "tokio", - "tracing", -] - -[[package]] -name = "actix-service" -version = "2.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b894941f818cfdc7ccc4b9e60fa7e53b5042a2e8567270f9147d5591893373a" -dependencies = [ - "futures-core", - "paste", - "pin-project-lite", -] - -[[package]] -name = "actix-tls" -version = "3.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac453898d866cdbecdbc2334fe1738c747b4eba14a677261f2b768ba05329389" -dependencies = [ - "actix-rt", - "actix-service", - "actix-utils", - "futures-core", - "impl-more", - "pin-project-lite", - "tokio", - "tokio-rustls 0.23.4", - "tokio-util", - "tracing", - "webpki-roots", -] - -[[package]] -name = "actix-utils" -version = "3.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88a1dcdff1466e3c2488e1cb5c36a71822750ad43839937f85d2f4d9f8b705d8" -dependencies = [ - "local-waker", - "pin-project-lite", -] - -[[package]] -name = "actix-web" -version = "4.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9180d76e5cc7ccbc4d60a506f2c727730b154010262df5b910eb17dbe4b8cb38" -dependencies = [ - "actix-codec", - "actix-http", - "actix-macros", - "actix-router", - "actix-rt", - "actix-server", - "actix-service", - "actix-tls", - "actix-utils", - "actix-web-codegen", - "ahash", - "bytes", - "bytestring", - "cfg-if", - "derive_more", - "encoding_rs", - "futures-core", - "futures-util", - "impl-more", - "itoa", - "language-tags", - "log", - "mime", - "once_cell", - "pin-project-lite", - "regex-lite", - "serde", - "serde_json", - "serde_urlencoded", - "smallvec", - "socket2", - "time", - "url", -] - -[[package]] -name = "actix-web-codegen" -version = "4.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f591380e2e68490b5dfaf1dd1aa0ebe78d84ba7067078512b4ea6e4492d622b8" -dependencies = [ - "actix-router", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "addr2line" -version = "0.22.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" -dependencies = [ - "gimli", -] - -[[package]] -name = "adler" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" - -[[package]] -name = "ahash" -version = "0.8.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" -dependencies = [ - "cfg-if", - "getrandom 0.2.15", - "once_cell", - "version_check", - "zerocopy", -] - -[[package]] -name = "aho-corasick" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" -dependencies = [ - "memchr", -] - -[[package]] -name = "android-tzdata" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" - [[package]] name = "android_system_properties" version = "0.1.5" @@ -275,37 +13,15 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.86" +version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" - -[[package]] -name = "async-channel" -version = "1.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35" -dependencies = [ - "concurrent-queue", - "event-listener 2.5.3", - "futures-core", -] - -[[package]] -name = "async-lock" -version = "3.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff6e472cdea888a4bd64f342f09b3f50e1886d32afe8df3d663c01140b811b18" -dependencies = [ - "event-listener 5.3.1", - "event-listener-strategy", - "pin-project-lite", -] +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" [[package]] name = "async-trait" -version = "0.1.81" +version = "0.1.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e0c28dcc82d7c8ead5cb13beb15405b57b8546e93215673ff8ca0349a028107" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", @@ -313,134 +29,16 @@ dependencies = [ ] [[package]] -name = "atty" -version = "0.2.14" +name = "atomic-waker" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" -dependencies = [ - "hermit-abi 0.1.19", - "libc", - "winapi", -] +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "autocfg" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" - -[[package]] -name = "azure_core" -version = "0.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34ce3de4b65b1ee2667c81d1fc692949049502a4cf9c38118d811d6d79a7eaef" -dependencies = [ - "async-trait", - "base64 0.22.1", - "bytes", - "dyn-clone", - "futures", - "getrandom 0.2.15", - "hmac 0.12.1", - "http-types", - "once_cell", - "paste", - "pin-project", - "quick-xml 0.31.0", - "rand 0.8.5", - "reqwest 0.12.5", - "rustc_version", - "serde", - "serde_json", - "sha2 0.10.8", - "time", - "tracing", - "url", - "uuid", -] - -[[package]] -name = "azure_storage" -version = "0.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9713002fc30956a9f4061cdbc2e912ff739c6160e138ad3b6d992b3bcedccc6d" -dependencies = [ - "RustyXML", - "async-lock", - "async-trait", - "azure_core", - "bytes", - "serde", - "serde_derive", - "time", - "tracing", - "url", - "uuid", -] - -[[package]] -name = "azure_storage_blobs" -version = "0.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5b3a31dd8f920739437b827d0c9f9a4011eb3f06f79a121764aa11af6c51ee2" -dependencies = [ - "RustyXML", - "azure_core", - "azure_storage", - "azure_svc_blobstorage", - "bytes", - "futures", - "serde", - "serde_derive", - "serde_json", - "time", - "tracing", - "url", - "uuid", -] - -[[package]] -name = "azure_svc_blobstorage" -version = "0.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ef37ba6180df451042f1c277d4d0898e2447f0a5d5072e0ff11ee6ea5e7ef38" -dependencies = [ - "azure_core", - "bytes", - "futures", - "log", - "once_cell", - "serde", - "serde_json", - "time", -] - -[[package]] -name = "backtrace" -version = "0.3.73" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a" -dependencies = [ - "addr2line", - "cc", - "cfg-if", - "libc", - "miniz_oxide", - "object", - "rustc-demangle", -] - -[[package]] -name = "base64" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" - -[[package]] -name = "base64" -version = "0.21.7" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "base64" @@ -450,24 +48,9 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - -[[package]] -name = "bitflags" -version = "2.6.0" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" - -[[package]] -name = "block-buffer" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" -dependencies = [ - "generic-array", -] +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" [[package]] name = "block-buffer" @@ -480,92 +63,73 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.16.0" +version = "3.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" - -[[package]] -name = "byteorder" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" [[package]] name = "bytes" -version = "1.7.1" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" [[package]] -name = "bytestring" -version = "1.3.1" +name = "cc" +version = "1.2.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74d80203ea6b29df88012294f62733de21cfeab47f17b41af3a38bc30a03ee72" +checksum = "7a0dd1ca384932ff3641c8718a02769f1698e7563dc6974ffd03346116310423" dependencies = [ - "bytes", + "find-msvc-tools", + "shlex", ] [[package]] -name = "cc" -version = "1.1.10" +name = "cfg-if" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9e8aabfac534be767c909e0690571677d49f41bd8465ae876fe043d52ba5292" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] -name = "cfg-if" -version = "1.0.0" +name = "cfg_aliases" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "chrono" -version = "0.4.38" +version = "0.4.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" dependencies = [ - "android-tzdata", "iana-time-zone", "js-sys", "num-traits", "serde", "wasm-bindgen", - "windows-targets 0.52.6", -] - -[[package]] -name = "common-s3-headers" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f177814b579c39ae2325720be922d21fab28ff22fe81aa27c79625326ce19db" -dependencies = [ - "hex", - "hmac 0.12.1", - "percent-encoding", - "sha2 0.10.8", - "time", - "url", + "windows-link", ] [[package]] -name = "concurrent-queue" -version = "2.5.0" +name = "console_error_panic_hook" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" dependencies = [ - "crossbeam-utils", + "cfg-if", + "wasm-bindgen", ] [[package]] -name = "convert_case" -version = "0.4.0" +name = "const-oid" +version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" [[package]] name = "core-foundation" -version = "0.9.4" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" dependencies = [ "core-foundation-sys", "libc", @@ -579,224 +143,63 @@ checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "cpufeatures" -version = "0.2.13" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51e852e6dc9a5bed1fae92dd2375037bf2b768725bf3be87811edee3249d09ad" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" dependencies = [ "libc", ] -[[package]] -name = "crc32fast" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "crossbeam-channel" -version = "0.5.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33480d6946193aa8033910124896ca395333cae7e2d1113d1fef6c3272217df2" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-epoch" -version = "0.9.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-utils" -version = "0.8.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" - [[package]] name = "crypto-common" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ "generic-array", "typenum", ] -[[package]] -name = "crypto-mac" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1d1a86f49236c215f271d40892d5fc950490551400b02ef360692c29815c714" -dependencies = [ - "generic-array", - "subtle", -] - -[[package]] -name = "ct-logs" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1a816186fa68d9e426e3cb4ae4dff1fcd8e4a2c34b781bf7a822574a0d0aac8" -dependencies = [ - "sct 0.6.1", -] - -[[package]] -name = "deranged" -version = "0.3.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" -dependencies = [ - "powerfmt", - "serde", -] - -[[package]] -name = "derive_more" -version = "0.99.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f33878137e4dafd7fa914ad4e259e18a4e8e532b9617a2d0150262bf53abfce" -dependencies = [ - "convert_case", - "proc-macro2", - "quote", - "rustc_version", - "syn", -] - -[[package]] -name = "digest" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" -dependencies = [ - "generic-array", -] - [[package]] name = "digest" version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "block-buffer 0.10.4", + "block-buffer", + "const-oid", "crypto-common", "subtle", ] [[package]] -name = "dirs-next" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" -dependencies = [ - "cfg-if", - "dirs-sys-next", -] - -[[package]] -name = "dirs-sys-next" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" -dependencies = [ - "libc", - "redox_users", - "winapi", -] - -[[package]] -name = "dyn-clone" -version = "1.0.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125" - -[[package]] -name = "encoding_rs" -version = "0.8.34" +name = "displaydoc" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ - "cfg-if", + "proc-macro2", + "quote", + "syn", ] [[package]] -name = "env_logger" -version = "0.9.3" +name = "either" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a12e6657c4c97ebab115a42dcee77225f7f482cdd841cf7088c657a42e9e00e7" -dependencies = [ - "atty", - "humantime", - "log", - "regex", - "termcolor", -] +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" [[package]] name = "equivalent" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" - -[[package]] -name = "errno" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" -dependencies = [ - "libc", - "windows-sys 0.52.0", -] - -[[package]] -name = "event-listener" -version = "2.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" - -[[package]] -name = "event-listener" -version = "5.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6032be9bd27023a771701cc49f9f053c751055f71efb2e0ae5c15809093675ba" -dependencies = [ - "concurrent-queue", - "parking", - "pin-project-lite", -] - -[[package]] -name = "event-listener-strategy" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f214dc438f977e6d4e3500aaa277f5ad94ca83fbbd9b1a15713ce2344ccc5a1" -dependencies = [ - "event-listener 5.3.1", - "pin-project-lite", -] - -[[package]] -name = "fastrand" -version = "1.9.0" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" -dependencies = [ - "instant", -] +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] -name = "fastrand" -version = "2.1.0" +name = "find-msvc-tools" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" [[package]] name = "fnv" @@ -805,34 +208,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] -name = "foreign-types" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" -dependencies = [ - "foreign-types-shared", -] - -[[package]] -name = "foreign-types-shared" -version = "0.1.1" +name = "foldhash" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" [[package]] name = "form_urlencoded" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" dependencies = [ "percent-encoding", ] [[package]] name = "futures" -version = "0.3.30" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" dependencies = [ "futures-channel", "futures-core", @@ -845,9 +239,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.30" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" dependencies = [ "futures-core", "futures-sink", @@ -855,15 +249,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.30" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" [[package]] name = "futures-executor" -version = "0.3.30" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" dependencies = [ "futures-core", "futures-task", @@ -871,31 +265,16 @@ dependencies = [ ] [[package]] -name = "futures-io" -version = "0.3.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" - -[[package]] -name = "futures-lite" -version = "1.13.0" +name = "futures-io" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce" -dependencies = [ - "fastrand 1.9.0", - "futures-core", - "futures-io", - "memchr", - "parking", - "pin-project-lite", - "waker-fn", -] +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" [[package]] name = "futures-macro" -version = "0.3.30" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" dependencies = [ "proc-macro2", "quote", @@ -904,21 +283,21 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.30" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" [[package]] name = "futures-task" -version = "0.3.30" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" [[package]] name = "futures-util" -version = "0.3.30" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ "futures-channel", "futures-core", @@ -928,7 +307,6 @@ dependencies = [ "futures-task", "memchr", "pin-project-lite", - "pin-utils", "slab", ] @@ -944,46 +322,56 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.1.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", + "js-sys", "libc", - "wasi 0.9.0+wasi-snapshot-preview1", + "wasi", + "wasm-bindgen", ] [[package]] name = "getrandom" -version = "0.2.15" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", "js-sys", "libc", - "wasi 0.11.0+wasi-snapshot-preview1", + "r-efi 5.3.0", + "wasip2", "wasm-bindgen", ] [[package]] -name = "gimli" -version = "0.29.0" +name = "getrandom" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "wasip2", + "wasip3", +] [[package]] name = "h2" -version = "0.3.26" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" +checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" dependencies = [ + "atomic-waker", "bytes", "fnv", "futures-core", "futures-sink", - "futures-util", - "http 0.2.12", + "http", "indexmap", "slab", "tokio", @@ -993,24 +381,24 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.14.5" +version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] [[package]] -name = "hermit-abi" -version = "0.1.19" +name = "hashbrown" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" -dependencies = [ - "libc", -] +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" [[package]] -name = "hermit-abi" -version = "0.3.9" +name = "heck" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "hex" @@ -1018,58 +406,25 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" -[[package]] -name = "hmac" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a2a2320eb7ec0ebe8da8f744d7812d9fc4cb4d09344ac01898dbcb6a20ae69b" -dependencies = [ - "crypto-mac", - "digest 0.9.0", -] - [[package]] name = "hmac" version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" dependencies = [ - "digest 0.10.7", -] - -[[package]] -name = "http" -version = "0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" -dependencies = [ - "bytes", - "fnv", - "itoa", + "digest", ] [[package]] name = "http" -version = "1.1.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" dependencies = [ "bytes", - "fnv", "itoa", ] -[[package]] -name = "http-body" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" -dependencies = [ - "bytes", - "http 0.2.12", - "pin-project-lite", -] - [[package]] name = "http-body" version = "1.0.1" @@ -1077,98 +432,51 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ "bytes", - "http 1.1.0", + "http", ] [[package]] name = "http-body-util" -version = "0.1.2" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" dependencies = [ "bytes", - "futures-util", - "http 1.1.0", - "http-body 1.0.1", - "pin-project-lite", -] - -[[package]] -name = "http-types" -version = "2.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e9b187a72d63adbfba487f48095306ac823049cb504ee195541e91c7775f5ad" -dependencies = [ - "anyhow", - "async-channel", - "base64 0.13.1", - "futures-lite", - "infer", + "futures-core", + "http", + "http-body", "pin-project-lite", - "rand 0.7.3", - "serde", - "serde_json", - "serde_qs", - "serde_urlencoded", - "url", ] [[package]] name = "httparse" -version = "1.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fcc0b4a115bf80b728eb8ea024ad5bd707b615bfed49e0665b6e0f86fd082d9" - -[[package]] -name = "httpdate" -version = "1.0.3" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" [[package]] name = "humantime" -version = "2.1.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" +checksum = "135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424" [[package]] name = "hyper" -version = "0.14.30" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a152ddd61dfaec7273fe8419ab357f33aee0d914c5f4efbf0d96fa749eea5ec9" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" dependencies = [ + "atomic-waker", "bytes", "futures-channel", "futures-core", - "futures-util", "h2", - "http 0.2.12", - "http-body 0.4.6", - "httparse", - "httpdate", - "itoa", - "pin-project-lite", - "socket2", - "tokio", - "tower-service", - "tracing", - "want", -] - -[[package]] -name = "hyper" -version = "1.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50dfd22e0e76d0f662d429a5f80fcaf3855009297eab6a0a9f8543834744ba05" -dependencies = [ - "bytes", - "futures-channel", - "futures-util", - "http 1.1.0", - "http-body 1.0.1", + "http", + "http-body", "httparse", "itoa", "pin-project-lite", + "pin-utils", "smallvec", "tokio", "want", @@ -1176,80 +484,55 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.22.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f9f7a97316d44c0af9b0301e65010573a853a9fc97046d7331d7f6bc0fd5a64" -dependencies = [ - "ct-logs", - "futures-util", - "hyper 0.14.30", - "log", - "rustls 0.19.1", - "rustls-native-certs", - "tokio", - "tokio-rustls 0.22.0", - "webpki 0.21.4", -] - -[[package]] -name = "hyper-tls" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" -dependencies = [ - "bytes", - "hyper 0.14.30", - "native-tls", - "tokio", - "tokio-native-tls", -] - -[[package]] -name = "hyper-tls" -version = "0.6.0" +version = "0.27.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" dependencies = [ - "bytes", - "http-body-util", - "hyper 1.4.1", + "http", + "hyper", "hyper-util", - "native-tls", + "rustls", + "rustls-native-certs", + "rustls-pki-types", "tokio", - "tokio-native-tls", + "tokio-rustls", "tower-service", ] [[package]] name = "hyper-util" -version = "0.1.7" +version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cde7055719c54e36e95e8719f95883f22072a48ede39db7fc17a4e1d5281e9b9" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" dependencies = [ + "base64", "bytes", "futures-channel", "futures-util", - "http 1.1.0", - "http-body 1.0.1", - "hyper 1.4.1", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", "pin-project-lite", "socket2", "tokio", - "tower", "tower-service", "tracing", ] [[package]] name = "iana-time-zone" -version = "0.1.60" +version = "0.1.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" dependencies = [ "android_system_properties", "core-foundation-sys", "iana-time-zone-haiku", "js-sys", + "log", "wasm-bindgen", "windows-core", ] @@ -1264,880 +547,722 @@ dependencies = [ ] [[package]] -name = "idna" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" -dependencies = [ - "unicode-bidi", - "unicode-normalization", -] - -[[package]] -name = "impl-more" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "206ca75c9c03ba3d4ace2460e57b189f39f43de612c2f85836e65c929701bb2d" - -[[package]] -name = "indexmap" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de3fc2e30ba82dd1b3911c8de1ffc143c74a914a14e99514d7637e3099df5ea0" -dependencies = [ - "equivalent", - "hashbrown", -] - -[[package]] -name = "infer" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64e9829a50b42bb782c1df523f78d332fe371b10c661e78b7a3c34b0198e9fac" - -[[package]] -name = "instant" -version = "0.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "ipnet" -version = "2.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" - -[[package]] -name = "itoa" -version = "1.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" - -[[package]] -name = "js-sys" -version = "0.3.70" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1868808506b929d7b0cfa8f75951347aa71bb21144b7791bae35d9bccfcfe37a" -dependencies = [ - "wasm-bindgen", -] - -[[package]] -name = "language-tags" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388" - -[[package]] -name = "lazy_static" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" - -[[package]] -name = "libc" -version = "0.2.155" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" - -[[package]] -name = "libredox" -version = "0.1.3" +name = "icu_collections" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" dependencies = [ - "bitflags 2.6.0", - "libc", + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", ] [[package]] -name = "linux-raw-sys" -version = "0.4.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" - -[[package]] -name = "local-channel" -version = "0.1.5" +name = "icu_locale_core" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6cbc85e69b8df4b8bb8b89ec634e7189099cea8927a276b7384ce5488e53ec8" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" dependencies = [ - "futures-core", - "futures-sink", - "local-waker", + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", ] [[package]] -name = "local-waker" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d873d7c67ce09b42110d801813efbc9364414e356be9935700d368351657487" - -[[package]] -name = "lock_api" -version = "0.4.12" +name = "icu_normalizer" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" dependencies = [ - "autocfg", - "scopeguard", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", ] [[package]] -name = "log" -version = "0.4.22" +name = "icu_normalizer_data" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" [[package]] -name = "md-5" -version = "0.9.1" +name = "icu_properties" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b5a279bb9607f9f53c22d496eade00d138d1bdcccd07d74650387cf94942a15" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" dependencies = [ - "block-buffer 0.9.0", - "digest 0.9.0", - "opaque-debug", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", ] [[package]] -name = "memchr" -version = "2.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" - -[[package]] -name = "mime" -version = "0.3.17" +name = "icu_properties_data" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" [[package]] -name = "miniz_oxide" -version = "0.7.4" +name = "icu_provider" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" dependencies = [ - "adler", + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", ] [[package]] -name = "mio" -version = "1.0.2" +name = "id-arena" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" -dependencies = [ - "hermit-abi 0.3.9", - "libc", - "log", - "wasi 0.11.0+wasi-snapshot-preview1", - "windows-sys 0.52.0", -] +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" [[package]] -name = "moka" -version = "0.12.8" +name = "idna" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32cf62eb4dd975d2dde76432fb1075c49e3ee2331cf36f1f8fd4b66550d32b6f" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" dependencies = [ - "async-lock", - "async-trait", - "crossbeam-channel", - "crossbeam-epoch", - "crossbeam-utils", - "event-listener 5.3.1", - "futures-util", - "once_cell", - "parking_lot", - "quanta", - "rustc_version", + "idna_adapter", "smallvec", - "tagptr", - "thiserror 1.0.63", - "triomphe", - "uuid", -] - -[[package]] -name = "native-tls" -version = "0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8614eb2c83d59d1c8cc974dd3f920198647674a0a035e1af1fa58707e317466" -dependencies = [ - "libc", - "log", - "openssl", - "openssl-probe", - "openssl-sys", - "schannel", - "security-framework", - "security-framework-sys", - "tempfile", -] - -[[package]] -name = "num-conv" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" - -[[package]] -name = "num-traits" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" -dependencies = [ - "autocfg", -] - -[[package]] -name = "object" -version = "0.36.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27b64972346851a39438c60b341ebc01bba47464ae329e55cf343eb93964efd9" -dependencies = [ - "memchr", + "utf8_iter", ] [[package]] -name = "once_cell" -version = "1.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" - -[[package]] -name = "opaque-debug" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" - -[[package]] -name = "openssl" -version = "0.10.66" +name = "idna_adapter" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9529f4786b70a3e8c61e11179af17ab6188ad8d0ded78c5529441ed39d4bd9c1" -dependencies = [ - "bitflags 2.6.0", - "cfg-if", - "foreign-types", - "libc", - "once_cell", - "openssl-macros", - "openssl-sys", +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", ] [[package]] -name = "openssl-macros" -version = "0.1.1" +name = "indexmap" +version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" dependencies = [ - "proc-macro2", - "quote", - "syn", + "equivalent", + "hashbrown 0.16.1", + "serde", + "serde_core", ] [[package]] -name = "openssl-probe" -version = "0.1.5" +name = "ipnet" +version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" [[package]] -name = "openssl-sys" -version = "0.9.103" +name = "iri-string" +version = "0.7.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f9e8deee91df40a943c71b917e5874b951d32a802526c85721ce3b776c929d6" +checksum = "d8e7418f59cc01c88316161279a7f665217ae316b388e58a0d10e29f54f1e5eb" dependencies = [ - "cc", - "libc", - "pkg-config", - "vcpkg", + "memchr", + "serde", ] [[package]] -name = "parking" -version = "2.2.0" +name = "itertools" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] [[package]] -name = "parking_lot" -version = "0.12.3" +name = "itoa" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" -dependencies = [ - "lock_api", - "parking_lot_core", -] +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] -name = "parking_lot_core" -version = "0.9.10" +name = "js-sys" +version = "0.3.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" dependencies = [ - "cfg-if", - "libc", - "redox_syscall", - "smallvec", - "windows-targets 0.52.6", + "once_cell", + "wasm-bindgen", ] [[package]] -name = "paste" -version = "1.0.15" +name = "leb128fmt" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] -name = "percent-encoding" -version = "2.3.1" +name = "libc" +version = "0.2.183" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" [[package]] -name = "pin-project" -version = "1.1.5" +name = "litemap" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3" -dependencies = [ - "pin-project-internal", -] +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" [[package]] -name = "pin-project-internal" -version = "1.1.5" +name = "lock_api" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" dependencies = [ - "proc-macro2", - "quote", - "syn", + "scopeguard", ] [[package]] -name = "pin-project-lite" -version = "0.2.14" +name = "log" +version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] -name = "pin-utils" -version = "0.1.0" +name = "lru-slab" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" [[package]] -name = "pkg-config" -version = "0.3.30" +name = "matchit" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" +checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" [[package]] -name = "powerfmt" -version = "0.2.0" +name = "matchit" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" +checksum = "2f926ade0c4e170215ae43342bf13b9310a437609c81f29f86c5df6657582ef9" [[package]] -name = "ppv-lite86" -version = "0.2.20" +name = "md-5" +version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" dependencies = [ - "zerocopy", + "cfg-if", + "digest", ] [[package]] -name = "proc-macro2" -version = "1.0.95" +name = "memchr" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" -dependencies = [ - "unicode-ident", -] +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] -name = "quanta" -version = "0.12.3" +name = "mio" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e5167a477619228a0b284fac2674e3c388cba90631d7b7de620e6f1fcd08da5" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" dependencies = [ - "crossbeam-utils", "libc", - "once_cell", - "raw-cpuid", - "wasi 0.11.0+wasi-snapshot-preview1", - "web-sys", - "winapi", + "wasi", + "windows-sys 0.61.2", ] [[package]] -name = "quick-xml" -version = "0.31.0" +name = "multistore" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1004a344b30a54e2ee58d66a71b32d2db2feb0a31f9a2d302bf0536f15de2a33" +checksum = "02623487c2ae2bb1662508ecbbd4825fdf47157fcc43053a92ecfab6d1721cdc" dependencies = [ - "memchr", + "async-trait", + "base64", + "bytes", + "chrono", + "futures", + "hex", + "hmac", + "http", + "matchit 0.8.6", + "object_store", + "percent-encoding", + "quick-xml 0.37.5", "serde", + "serde_json", + "sha2", + "thiserror", + "tracing", + "url", + "uuid", ] [[package]] -name = "quick-xml" -version = "0.36.1" +name = "multistore-cf-workers" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96a05e2e8efddfa51a84ca47cec303fac86c8541b686d37cac5efc0e094417bc" +checksum = "0bf468de19f74cc94eb062b9d07a2b584401b7ed368c58fa15e0434df7c432e6" dependencies = [ - "memchr", - "serde", + "async-trait", + "bytes", + "futures", + "getrandom 0.2.17", + "getrandom 0.3.4", + "http", + "http-body", + "http-body-util", + "js-sys", + "multistore", + "object_store", + "tracing", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "worker", ] [[package]] -name = "quote" -version = "1.0.36" +name = "multistore-path-mapping" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" +checksum = "6727e4df8935d72848730c4bb44bb0f2a2cfd52532a405c153d924882cb8b79a" dependencies = [ - "proc-macro2", + "multistore", + "tracing", ] [[package]] -name = "rand" -version = "0.7.3" +name = "num-traits" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ - "getrandom 0.1.16", - "libc", - "rand_chacha 0.2.2", - "rand_core 0.5.1", - "rand_hc", + "autocfg", ] [[package]] -name = "rand" -version = "0.8.5" +name = "object_store" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +checksum = "c2858065e55c148d294a9f3aae3b0fa9458edadb41a108397094566f4e3c0dfb" dependencies = [ - "libc", - "rand_chacha 0.3.1", - "rand_core 0.6.4", + "async-trait", + "base64", + "bytes", + "chrono", + "form_urlencoded", + "futures", + "http", + "http-body-util", + "httparse", + "humantime", + "hyper", + "itertools", + "md-5", + "parking_lot", + "percent-encoding", + "quick-xml 0.38.4", + "rand", + "reqwest", + "ring", + "serde", + "serde_json", + "serde_urlencoded", + "thiserror", + "tokio", + "tracing", + "url", + "wasm-bindgen-futures", + "web-time", ] [[package]] -name = "rand_chacha" -version = "0.2.2" +name = "once_cell" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" -dependencies = [ - "ppv-lite86", - "rand_core 0.5.1", -] +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] -name = "rand_chacha" -version = "0.3.1" +name = "openssl-probe" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core 0.6.4", -] +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" [[package]] -name = "rand_core" -version = "0.5.1" +name = "parking_lot" +version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" dependencies = [ - "getrandom 0.1.16", + "lock_api", + "parking_lot_core", ] [[package]] -name = "rand_core" -version = "0.6.4" +name = "parking_lot_core" +version = "0.9.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ - "getrandom 0.2.15", + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", ] [[package]] -name = "rand_hc" -version = "0.2.0" +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project" +version = "1.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +checksum = "f1749c7ed4bcaf4c3d0a3efc28538844fb29bcdd7d2b67b2be7e20ba861ff517" dependencies = [ - "rand_core 0.5.1", + "pin-project-internal", ] [[package]] -name = "raw-cpuid" -version = "11.2.0" +name = "pin-project-internal" +version = "1.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ab240315c661615f2ee9f0f2cd32d5a7343a84d5ebcccb99d46e6637565e7b0" +checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6" dependencies = [ - "bitflags 2.6.0", + "proc-macro2", + "quote", + "syn", ] [[package]] -name = "redox_syscall" -version = "0.5.3" +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "potential_utf" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a908a6e00f1fdd0dfd9c0eb08ce85126f6d8bbda50017e74bc4a4b7d4a926a4" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" dependencies = [ - "bitflags 2.6.0", + "zerovec", ] [[package]] -name = "redox_users" -version = "0.4.5" +name = "ppv-lite86" +version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd283d9651eeda4b2a83a43c1c91b266c40fd76ecd39a50a8c630ae69dc72891" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" dependencies = [ - "getrandom 0.2.15", - "libredox", - "thiserror 1.0.63", + "zerocopy", ] [[package]] -name = "regex" -version = "1.10.6" +name = "prettyplease" +version = "0.2.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ - "aho-corasick", - "memchr", - "regex-automata", - "regex-syntax", + "proc-macro2", + "syn", ] [[package]] -name = "regex-automata" -version = "0.4.7" +name = "proc-macro2" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", + "unicode-ident", ] [[package]] -name = "regex-lite" -version = "0.1.6" +name = "quick-xml" +version = "0.37.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53a49587ad06b26609c52e423de037e7f57f20d53535d66e08c695f347df952a" +checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb" +dependencies = [ + "memchr", + "serde", +] [[package]] -name = "regex-syntax" -version = "0.8.4" +name = "quick-xml" +version = "0.38.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" +checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c" +dependencies = [ + "memchr", + "serde", +] [[package]] -name = "reqwest" -version = "0.11.27" +name = "quinn" +version = "0.11.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" dependencies = [ - "base64 0.21.7", "bytes", - "encoding_rs", - "futures-core", - "futures-util", - "h2", - "http 0.2.12", - "http-body 0.4.6", - "hyper 0.14.30", - "hyper-tls 0.5.0", - "ipnet", - "js-sys", - "log", - "mime", - "native-tls", - "once_cell", - "percent-encoding", + "cfg_aliases", "pin-project-lite", - "rustls-pemfile 1.0.4", - "serde", - "serde_json", - "serde_urlencoded", - "sync_wrapper 0.1.2", - "system-configuration", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror", "tokio", - "tokio-native-tls", - "tokio-util", - "tower-service", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "wasm-streams", - "web-sys", - "winreg 0.50.0", + "tracing", + "web-time", ] [[package]] -name = "reqwest" -version = "0.12.5" +name = "quinn-proto" +version = "0.11.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7d6d2a27d57148378eb5e111173f4276ad26340ecc5c49a4a2152167a2d6a37" +checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" dependencies = [ - "base64 0.22.1", "bytes", - "futures-core", - "futures-util", - "http 1.1.0", - "http-body 1.0.1", - "http-body-util", - "hyper 1.4.1", - "hyper-tls 0.6.0", - "hyper-util", - "ipnet", - "js-sys", - "log", - "mime", - "native-tls", - "once_cell", - "percent-encoding", - "pin-project-lite", - "rustls-pemfile 2.1.3", - "serde", - "serde_json", - "serde_urlencoded", - "sync_wrapper 1.0.1", - "tokio", - "tokio-native-tls", - "tokio-util", - "tower-service", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "wasm-streams", - "web-sys", - "winreg 0.52.0", + "getrandom 0.3.4", + "lru-slab", + "rand", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror", + "tinyvec", + "tracing", + "web-time", ] [[package]] -name = "ring" -version = "0.16.20" +name = "quinn-udp" +version = "0.5.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" dependencies = [ - "cc", + "cfg_aliases", "libc", "once_cell", - "spin 0.5.2", - "untrusted 0.7.1", - "web-sys", - "winapi", + "socket2", + "tracing", + "windows-sys 0.60.2", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", ] [[package]] -name = "ring" -version = "0.17.8" +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" dependencies = [ - "cc", - "cfg-if", - "getrandom 0.2.15", - "libc", - "spin 0.9.8", - "untrusted 0.9.0", - "windows-sys 0.52.0", + "rand_chacha", + "rand_core", ] [[package]] -name = "rusoto_core" -version = "0.47.0" +name = "rand_chacha" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b4f000e8934c1b4f70adde180056812e7ea6b1a247952db8ee98c94cd3116cc" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ - "async-trait", - "base64 0.13.1", - "bytes", - "crc32fast", - "futures", - "http 0.2.12", - "hyper 0.14.30", - "hyper-rustls", - "lazy_static", - "log", - "rusoto_credential", - "rusoto_signature", - "rustc_version", - "serde", - "serde_json", - "tokio", - "xml-rs", + "ppv-lite86", + "rand_core", ] [[package]] -name = "rusoto_credential" -version = "0.47.0" +name = "rand_core" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a46b67db7bb66f5541e44db22b0a02fed59c9603e146db3a9e633272d3bac2f" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" dependencies = [ - "async-trait", - "chrono", - "dirs-next", - "futures", - "hyper 0.14.30", - "serde", - "serde_json", - "shlex", - "tokio", - "zeroize", + "getrandom 0.3.4", ] [[package]] -name = "rusoto_s3" -version = "0.47.0" +name = "redox_syscall" +version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "048c2fe811a823ad5a9acc976e8bf4f1d910df719dcf44b15c3e96c5b7a51027" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "async-trait", - "bytes", - "futures", - "rusoto_core", - "xml-rs", + "bitflags", ] [[package]] -name = "rusoto_signature" -version = "0.47.0" +name = "reqwest" +version = "0.12.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6264e93384b90a747758bcc82079711eacf2e755c3a8b5091687b5349d870bcc" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" dependencies = [ - "base64 0.13.1", + "base64", "bytes", - "chrono", - "digest 0.9.0", - "futures", - "hex", - "hmac 0.11.0", - "http 0.2.12", - "hyper 0.14.30", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", "log", - "md-5", "percent-encoding", "pin-project-lite", - "rusoto_credential", - "rustc_version", + "quinn", + "rustls", + "rustls-native-certs", + "rustls-pki-types", "serde", - "sha2 0.9.9", + "serde_json", + "serde_urlencoded", + "sync_wrapper", "tokio", + "tokio-rustls", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", ] [[package]] -name = "rustc-demangle" -version = "0.1.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" - -[[package]] -name = "rustc_version" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" -dependencies = [ - "semver", -] - -[[package]] -name = "rustix" -version = "0.38.34" +name = "ring" +version = "0.17.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ - "bitflags 2.6.0", - "errno", + "cc", + "cfg-if", + "getrandom 0.2.17", "libc", - "linux-raw-sys", + "untrusted", "windows-sys 0.52.0", ] [[package]] -name = "rustls" -version = "0.19.1" +name = "rustc-hash" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35edb675feee39aec9c99fa5ff985081995a06d594114ae14cbe797ad7b7a6d7" -dependencies = [ - "base64 0.13.1", - "log", - "ring 0.16.20", - "sct 0.6.1", - "webpki 0.21.4", -] +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" [[package]] name = "rustls" -version = "0.20.9" +version = "0.23.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b80e3dec595989ea8510028f30c408a4630db12c9cbb8de34203b89d6577e99" +checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" dependencies = [ - "log", - "ring 0.16.20", - "sct 0.7.1", - "webpki 0.22.4", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", ] [[package]] name = "rustls-native-certs" -version = "0.5.0" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a07b7c1885bd8ed3831c289b7870b13ef46fe0e856d288c30d9cc17d75a2092" +checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" dependencies = [ "openssl-probe", - "rustls 0.19.1", + "rustls-pki-types", "schannel", "security-framework", ] [[package]] -name = "rustls-pemfile" -version = "1.0.4" +name = "rustls-pki-types" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" dependencies = [ - "base64 0.21.7", + "web-time", + "zeroize", ] [[package]] -name = "rustls-pemfile" -version = "2.1.3" +name = "rustls-webpki" +version = "0.103.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "196fe16b00e106300d3e45ecfcb764fa292a535d7326a29a5875c579c7417425" +checksum = "df33b2b81ac578cabaf06b89b0631153a3f416b0a886e8a7a1707fb51abbd1ef" dependencies = [ - "base64 0.22.1", + "ring", "rustls-pki-types", + "untrusted", ] [[package]] -name = "rustls-pki-types" -version = "1.8.0" +name = "rustversion" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc0a2ce646f8655401bb81e7927b812614bd5d91dbc968696be50603510fcaf0" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "ryu" -version = "1.0.18" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" [[package]] name = "schannel" -version = "0.1.23" +version = "0.1.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -2146,33 +1271,13 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" -[[package]] -name = "sct" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b362b83898e0e69f38515b82ee15aa80636befe47c3b6d3d89a911e78fc228ce" -dependencies = [ - "ring 0.16.20", - "untrusted 0.7.1", -] - -[[package]] -name = "sct" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" -dependencies = [ - "ring 0.17.8", - "untrusted 0.9.0", -] - [[package]] name = "security-framework" -version = "2.11.1" +version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ - "bitflags 2.6.0", + "bitflags", "core-foundation", "core-foundation-sys", "libc", @@ -2181,9 +1286,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.11.1" +version = "2.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75da29fe9b9b08fe9d6b22b5b4bcbc75d8db3aa31e639aa56bb62e9d46bfceaf" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" dependencies = [ "core-foundation-sys", "libc", @@ -2191,36 +1296,45 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.23" +version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" [[package]] name = "serde" -version = "1.0.207" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5665e14a49a4ea1b91029ba7d3bca9f299e1f7cfa194388ccc20f14743e784f2" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" dependencies = [ + "serde_core", "serde_derive", ] [[package]] -name = "serde-xml-rs" -version = "0.6.0" +name = "serde-wasm-bindgen" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb3aa78ecda1ebc9ec9847d5d3aba7d618823446a049ba2491940506da6e2782" +checksum = "8302e169f0eddcc139c70f139d19d6467353af16f9fce27e8c30158036a1e16b" dependencies = [ - "log", + "js-sys", "serde", - "thiserror 1.0.63", - "xml-rs", + "wasm-bindgen", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.207" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6aea2634c86b0e8ef2cfdc0c340baede54ec27b1e46febd7f80dffb2aa44a00e" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", @@ -2229,25 +1343,15 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.141" +version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30b9eff21ebe718216c6ec64e1d9ac57087aad11efc64e32002bce4a0d4c03d3" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" dependencies = [ "itoa", "memchr", - "ryu", - "serde", -] - -[[package]] -name = "serde_qs" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7715380eec75f029a4ef7de39a9200e0a63823176b759d055b613f5a87df6a6" -dependencies = [ - "percent-encoding", "serde", - "thiserror 1.0.63", + "serde_core", + "zmij", ] [[package]] @@ -2262,39 +1366,15 @@ dependencies = [ "serde", ] -[[package]] -name = "sha1" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest 0.10.7", -] - -[[package]] -name = "sha2" -version = "0.9.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" -dependencies = [ - "block-buffer 0.9.0", - "cfg-if", - "cpufeatures", - "digest 0.9.0", - "opaque-debug", -] - [[package]] name = "sha2" -version = "0.10.8" +version = "0.10.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", "cpufeatures", - "digest 0.10.7", + "digest", ] [[package]] @@ -2303,104 +1383,65 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" -[[package]] -name = "signal-hook-registry" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" -dependencies = [ - "libc", -] - [[package]] name = "slab" -version = "0.4.9" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" -dependencies = [ - "autocfg", -] +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "smallvec" -version = "1.13.2" +version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "socket2" -version = "0.5.7" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] name = "source-data-proxy" -version = "1.1.0" +version = "2.0.0" dependencies = [ - "actix-cors", - "actix-http", - "actix-web", - "async-trait", - "azure_core", - "azure_storage", - "azure_storage_blobs", - "bytes", - "chrono", - "common-s3-headers", - "env_logger", + "console_error_panic_hook", "futures", - "futures-core", - "futures-util", - "hex", - "hmac 0.12.1", - "log", - "moka", + "http", + "multistore", + "multistore-cf-workers", + "multistore-path-mapping", "percent-encoding", - "pin-project-lite", - "quick-xml 0.36.1", - "reqwest 0.11.27", - "rusoto_core", - "rusoto_credential", - "rusoto_s3", "serde", - "serde-xml-rs", "serde_json", - "sha2 0.10.8", - "thiserror 2.0.12", - "time", - "tokio", - "tokio-util", - "url", - "xml-rs", + "tracing", + "wasm-bindgen-futures", + "web-sys", + "worker", + "worker-macros", ] [[package]] -name = "spin" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" - -[[package]] -name = "spin" -version = "0.9.8" +name = "stable_deref_trait" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] name = "subtle" -version = "2.4.1" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.101" +version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", @@ -2409,99 +1450,38 @@ dependencies = [ [[package]] name = "sync_wrapper" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" - -[[package]] -name = "sync_wrapper" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" - -[[package]] -name = "system-configuration" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" -dependencies = [ - "bitflags 1.3.2", - "core-foundation", - "system-configuration-sys", -] - -[[package]] -name = "system-configuration-sys" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "tagptr" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" - -[[package]] -name = "tempfile" -version = "3.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04cbcdd0c794ebb0d4cf35e88edd2f7d2c4c3e9a5a6dab322839b321c6a87a64" -dependencies = [ - "cfg-if", - "fastrand 2.1.0", - "once_cell", - "rustix", - "windows-sys 0.59.0", -] - -[[package]] -name = "termcolor" -version = "1.4.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" dependencies = [ - "winapi-util", + "futures-core", ] [[package]] -name = "thiserror" -version = "1.0.63" +name = "synstructure" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ - "thiserror-impl 1.0.63", + "proc-macro2", + "quote", + "syn", ] [[package]] name = "thiserror" -version = "2.0.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" -dependencies = [ - "thiserror-impl 2.0.12", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.63" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ - "proc-macro2", - "quote", - "syn", + "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "2.0.12" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", @@ -2509,42 +1489,20 @@ dependencies = [ ] [[package]] -name = "time" -version = "0.3.36" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" -dependencies = [ - "deranged", - "itoa", - "js-sys", - "num-conv", - "powerfmt", - "serde", - "time-core", - "time-macros", -] - -[[package]] -name = "time-core" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" - -[[package]] -name = "time-macros" -version = "0.2.18" +name = "tinystr" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" dependencies = [ - "num-conv", - "time-core", + "displaydoc", + "zerovec", ] [[package]] name = "tinyvec" -version = "1.8.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" dependencies = [ "tinyvec_macros", ] @@ -2557,89 +1515,82 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.39.2" +version = "1.50.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daa4fb1bc778bd6f04cbfc4bb2d06a7396a8f299dc33ea1900cedaa316f467b1" +checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" dependencies = [ - "backtrace", "bytes", "libc", "mio", - "parking_lot", "pin-project-lite", - "signal-hook-registry", "socket2", "tokio-macros", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] name = "tokio-macros" -version = "2.4.0" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" +checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c" dependencies = [ "proc-macro2", "quote", "syn", ] -[[package]] -name = "tokio-native-tls" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" -dependencies = [ - "native-tls", - "tokio", -] - [[package]] name = "tokio-rustls" -version = "0.22.0" +version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc6844de72e57df1980054b38be3a9f4702aba4858be64dd700181a8a6d0e1b6" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" dependencies = [ - "rustls 0.19.1", + "rustls", "tokio", - "webpki 0.21.4", ] [[package]] -name = "tokio-rustls" -version = "0.23.4" +name = "tokio-util" +version = "0.7.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c43ee83903113e03984cb9e5cebe6c04a5116269e900e3ddba8f068a62adda59" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" dependencies = [ - "rustls 0.20.9", + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", "tokio", - "webpki 0.22.4", ] [[package]] -name = "tokio-util" -version = "0.7.11" +name = "tower" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cf6b47b3771c49ac75ad09a6162f53ad4b8088b76ac60e8ec1455b31a189fe1" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" dependencies = [ - "bytes", "futures-core", - "futures-sink", + "futures-util", "pin-project-lite", + "sync_wrapper", "tokio", + "tower-layer", + "tower-service", ] [[package]] -name = "tower" -version = "0.4.13" +name = "tower-http" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" dependencies = [ - "futures-core", + "bitflags", + "bytes", "futures-util", - "pin-project", + "http", + "http-body", + "iri-string", "pin-project-lite", - "tokio", + "tower", "tower-layer", "tower-service", ] @@ -2658,11 +1609,10 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" -version = "0.1.40" +version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ - "log", "pin-project-lite", "tracing-attributes", "tracing-core", @@ -2670,9 +1620,9 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.27" +version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", @@ -2681,19 +1631,13 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.32" +version = "0.1.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" dependencies = [ "once_cell", ] -[[package]] -name = "triomphe" -version = "0.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "859eb650cfee7434994602c3a68b25d77ad9e68c8a6cd491616ef86661382eb3" - [[package]] name = "try-lock" version = "0.2.5" @@ -2702,36 +1646,21 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "typenum" -version = "1.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" - -[[package]] -name = "unicode-bidi" -version = "0.3.15" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" [[package]] name = "unicode-ident" -version = "1.0.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" - -[[package]] -name = "unicode-normalization" -version = "0.1.23" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" -dependencies = [ - "tinyvec", -] +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] -name = "untrusted" -version = "0.7.1" +name = "unicode-xid" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" [[package]] name = "untrusted" @@ -2741,9 +1670,9 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.5.2" +version = "2.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" dependencies = [ "form_urlencoded", "idna", @@ -2752,20 +1681,21 @@ dependencies = [ ] [[package]] -name = "uuid" -version = "1.10.0" +name = "utf8_iter" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314" -dependencies = [ - "getrandom 0.2.15", - "serde", -] +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] -name = "vcpkg" -version = "0.2.15" +name = "uuid" +version = "1.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" +checksum = "a68d3c8f01c0cfa54a75291d83601161799e4a89a39e0929f4b0354d88757a37" +dependencies = [ + "getrandom 0.4.2", + "js-sys", + "wasm-bindgen", +] [[package]] name = "version_check" @@ -2773,12 +1703,6 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" -[[package]] -name = "waker-fn" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "317211a0dc0ceedd78fb2ca9a44aed3d7b9b26f81870d485c07122b4350673b7" - [[package]] name = "want" version = "0.3.1" @@ -2790,59 +1714,60 @@ dependencies = [ [[package]] name = "wasi" -version = "0.9.0+wasi-snapshot-preview1" +version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] -name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" +name = "wasip2" +version = "1.0.2+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] [[package]] -name = "wasm-bindgen" -version = "0.2.93" +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a82edfc16a6c469f5f44dc7b571814045d60404b55a0ee849f9bcfa2e63dd9b5" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" dependencies = [ - "cfg-if", - "once_cell", - "wasm-bindgen-macro", + "wit-bindgen", ] [[package]] -name = "wasm-bindgen-backend" -version = "0.2.93" +name = "wasm-bindgen" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9de396da306523044d3302746f1208fa71d7532227f15e347e2d93e4145dd77b" +checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" dependencies = [ - "bumpalo", - "log", + "cfg-if", "once_cell", - "proc-macro2", - "quote", - "syn", + "rustversion", + "wasm-bindgen-macro", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.43" +version = "0.4.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61e9300f63a621e96ed275155c108eb6f843b6a26d053f122ab69724559dc8ed" +checksum = "e9c5522b3a28661442748e09d40924dfb9ca614b21c00d3fd135720e48b67db8" dependencies = [ "cfg-if", + "futures-util", "js-sys", + "once_cell", "wasm-bindgen", "web-sys", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.93" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "585c4c91a46b072c92e908d99cb1dcdf95c5218eeb6f3bf1efa991ee7a68cccf" +checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2850,28 +1775,53 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.93" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836" +checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" dependencies = [ + "bumpalo", "proc-macro2", "quote", "syn", - "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.93" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c62a0a307cb4a311d3a07867860911ca130c3494e8c2719593806c08bc5d0484" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] [[package]] name = "wasm-streams" -version = "0.4.0" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b65dc4c90b63b118468cf747d8bf3566c1913ef60be765b5730ead9e0a3ba129" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" dependencies = [ "futures-util", "js-sys", @@ -2881,91 +1831,94 @@ dependencies = [ ] [[package]] -name = "web-sys" -version = "0.3.70" +name = "wasmparser" +version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26fdeaafd9bd129f65e7c031593c24d62186301e0c72c8978fa1678be7d532c0" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" dependencies = [ - "js-sys", - "wasm-bindgen", + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", ] [[package]] -name = "webpki" -version = "0.21.4" +name = "web-sys" +version = "0.3.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8e38c0608262c46d4a56202ebabdeb094cef7e560ca7a226c6bf055188aa4ea" +checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9" dependencies = [ - "ring 0.16.20", - "untrusted 0.7.1", + "js-sys", + "wasm-bindgen", ] [[package]] -name = "webpki" -version = "0.22.4" +name = "web-time" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed63aea5ce73d0ff405984102c42de94fc55a6b75765d621c65262469b3c9b53" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" dependencies = [ - "ring 0.17.8", - "untrusted 0.9.0", + "js-sys", + "wasm-bindgen", ] [[package]] -name = "webpki-roots" -version = "0.22.6" +name = "windows-core" +version = "0.62.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c71e40d7d2c34a5106301fb632274ca37242cd0c9d3e64dbece371a40a2d87" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" dependencies = [ - "webpki 0.22.4", + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", ] [[package]] -name = "winapi" -version = "0.3.9" +name = "windows-implement" +version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", + "proc-macro2", + "quote", + "syn", ] [[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" +name = "windows-interface" +version = "0.59.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-util" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ - "windows-sys 0.59.0", + "proc-macro2", + "quote", + "syn", ] [[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" +name = "windows-link" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] -name = "windows-core" -version = "0.52.0" +name = "windows-result" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" dependencies = [ - "windows-targets 0.52.6", + "windows-link", ] [[package]] -name = "windows-sys" -version = "0.48.0" +name = "windows-strings" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" dependencies = [ - "windows-targets 0.48.5", + "windows-link", ] [[package]] @@ -2979,26 +1932,20 @@ dependencies = [ [[package]] name = "windows-sys" -version = "0.59.0" +version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" dependencies = [ - "windows-targets 0.52.6", + "windows-targets 0.53.5", ] [[package]] -name = "windows-targets" -version = "0.48.5" +name = "windows-sys" +version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ - "windows_aarch64_gnullvm 0.48.5", - "windows_aarch64_msvc 0.48.5", - "windows_i686_gnu 0.48.5", - "windows_i686_msvc 0.48.5", - "windows_x86_64_gnu 0.48.5", - "windows_x86_64_gnullvm 0.48.5", - "windows_x86_64_msvc 0.48.5", + "windows-link", ] [[package]] @@ -3010,7 +1957,7 @@ dependencies = [ "windows_aarch64_gnullvm 0.52.6", "windows_aarch64_msvc 0.52.6", "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm", + "windows_i686_gnullvm 0.52.6", "windows_i686_msvc 0.52.6", "windows_x86_64_gnu 0.52.6", "windows_x86_64_gnullvm 0.52.6", @@ -3018,10 +1965,21 @@ dependencies = [ ] [[package]] -name = "windows_aarch64_gnullvm" -version = "0.48.5" +name = "windows-targets" +version = "0.53.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] [[package]] name = "windows_aarch64_gnullvm" @@ -3030,10 +1988,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] -name = "windows_aarch64_msvc" -version = "0.48.5" +name = "windows_aarch64_gnullvm" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" [[package]] name = "windows_aarch64_msvc" @@ -3042,10 +2000,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] -name = "windows_i686_gnu" -version = "0.48.5" +name = "windows_aarch64_msvc" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" [[package]] name = "windows_i686_gnu" @@ -3053,6 +2011,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + [[package]] name = "windows_i686_gnullvm" version = "0.52.6" @@ -3060,10 +2024,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] -name = "windows_i686_msvc" -version = "0.48.5" +name = "windows_i686_gnullvm" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" [[package]] name = "windows_i686_msvc" @@ -3072,10 +2036,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] -name = "windows_x86_64_gnu" -version = "0.48.5" +name = "windows_i686_msvc" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" [[package]] name = "windows_x86_64_gnu" @@ -3084,10 +2048,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] -name = "windows_x86_64_gnullvm" -version = "0.48.5" +name = "windows_x86_64_gnu" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" [[package]] name = "windows_x86_64_gnullvm" @@ -3096,10 +2060,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] -name = "windows_x86_64_msvc" -version = "0.48.5" +name = "windows_x86_64_gnullvm" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" [[package]] name = "windows_x86_64_msvc" @@ -3108,54 +2072,268 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] -name = "winreg" -version = "0.50.0" +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "wit-bindgen" +version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" dependencies = [ - "cfg-if", - "windows-sys 0.48.0", + "wit-bindgen-rust-macro", ] [[package]] -name = "winreg" -version = "0.52.0" +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "worker" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "244647fd7673893058f91f22a0eabd0f45bb50298e995688cb0c4b9837081b19" +dependencies = [ + "async-trait", + "bytes", + "chrono", + "futures-channel", + "futures-util", + "http", + "http-body", + "js-sys", + "matchit 0.7.3", + "pin-project", + "serde", + "serde-wasm-bindgen", + "serde_json", + "serde_urlencoded", + "tokio", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", + "worker-macros", + "worker-sys", +] + +[[package]] +name = "worker-macros" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac7e73ffb164183b57bb67d3efb881681fcd93ef5515ba32a4d022c4a6acc2ce" +dependencies = [ + "async-trait", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-bindgen-macro-support", + "worker-sys", +] + +[[package]] +name = "worker-sys" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a277a57398d4bfa075df44f501a17cfdf8542d224f0d36095a2adc7aee4ef0a5" +checksum = "4777582bf8a04174a034cb336f3702eb0e5cb444a67fdaa4fd44454ff7e2dd95" dependencies = [ "cfg-if", - "windows-sys 0.48.0", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "writeable" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" + +[[package]] +name = "yoke" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", ] [[package]] -name = "xml-rs" -version = "0.8.21" +name = "yoke-derive" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "539a77ee7c0de333dcc6da69b177380a0b81e0dacfa4f7344c465a36871ee601" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] [[package]] name = "zerocopy" -version = "0.7.35" +version = "0.8.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +checksum = "efbb2a062be311f2ba113ce66f697a4dc589f85e78a4aea276200804cea0ed87" dependencies = [ - "byteorder", "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.7.35" +version = "0.8.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e8bc7269b54418e7aeeef514aa68f8690b8c0489a06b0136e5f57c4c5ccab89" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", "syn", + "synstructure", ] [[package]] name = "zeroize" -version = "1.8.1" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + +[[package]] +name = "zerotrie" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zmij" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml index 3eeeae0..4400c74 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,54 +1,51 @@ [package] +authors = ["Anthony Lukach "] +edition = "2021" name = "source-data-proxy" +version = "2.0.0" -version = "1.1.0" -edition = "2021" +[lib] +crate-type = ["cdylib"] +# The cdylib depends on wasm-only crates (worker, wasm-bindgen-futures, web-sys) +# that don't compile on native targets, so we disable `cargo test` for the lib. +test = false + +[[test]] +name = "routing" +path = "tests/routing.rs" -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[[test]] +name = "pagination" +path = "tests/pagination.rs" [dependencies] +# Multistore +multistore = { version = "0.2.0", features = ["azure"] } +multistore-path-mapping = "0.2.0" -quick-xml = { version = "=0.36.1", features = ["serialize"] } -actix-web = { version = "^4", features = [ - "rustls", - "macros", -], default-features = false } -rusoto_core = { version = "0.47", default-features = false, features = [ - "rustls", -] } -rusoto_s3 = { version = "0.47", default-features = false, features = [ - "rustls", -] } -rusoto_credential = { version = "0.47" } -tokio-util = { version = "0.7", features = ["codec"] } -tokio = { version = "1", features = ["full"] } -futures-util = "0.3" -xml-rs = "0.8" -serde = { version = "1.0", features = ["derive"] } -serde-xml-rs = "0.6" -bytes = "1.0" -pin-project-lite = "0.2" +# Serialization +serde = { version = "1", features = ["derive"] } +serde_json = "1" + +# HTTP +http = "1" +percent-encoding = "2" + +# Tracing +tracing = "0.1" + +# Wasm-only dependencies (Cloudflare Workers runtime) +[target.'cfg(target_arch = "wasm32")'.dependencies] +multistore-cf-workers = { version = "0.2.0", features = ["azure"] } futures = "0.3" -futures-core = "0.3" -log = "0.4" -env_logger = "0.9" -chrono = { version = "0.4", features = ["serde"] } -async-trait = "0.1.81" -azure_storage_blobs = "0.20.0" -azure_storage = "0.20.0" -azure_core = "0.20.0" -time = { version = "0.3", features = ["formatting"] } -url = "2.2.2" -reqwest = { version = "0.11.0", features = ["stream", "json"] } -actix-cors = "0.7.0" -moka = { version = "0.12.8", features = ["future"] } -percent-encoding = "2.1.0" -sha2 = "0.10.6" -hex = "0.4.3" -hmac = "0.12" -actix-http = "^3" -thiserror = "2.0.12" -serde_json = "1.0.141" - -[dev-dependencies] -common-s3-headers = "1.0.0" +wasm-bindgen-futures = "0.4" +console_error_panic_hook = "0.1" +web-sys = { version = "0.3", features = [ + "Headers", + "ReadableStream", + "Request", + "Response", + "ResponseInit", +] } +worker = { version = "=0.7.4", features = ["http"] } +worker-macros = { version = "=0.7.4", features = ["http"] } diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index f8fc9b8..0000000 --- a/Dockerfile +++ /dev/null @@ -1,49 +0,0 @@ -# Build stage - target x86_64 for ECS Fargate compatibility -# Use bookworm variant to match runtime stage GLIBC version -FROM --platform=linux/amd64 rust:1.90.0-bookworm AS builder - -# Set environment variables for consistent builds -ENV CARGO_TARGET_DIR=/app/target -ENV RUSTFLAGS="-C target-cpu=x86-64" - -# Copy source code -COPY . /app -WORKDIR /app - -# Add x86_64 target and build -RUN rustup target add x86_64-unknown-linux-gnu -RUN cargo build --release --target x86_64-unknown-linux-gnu - -# Runtime stage - minimal Debian image -FROM --platform=linux/amd64 debian:bookworm-slim AS runtime - -# Install runtime dependencies (ca-certificates for HTTPS requests) -RUN apt-get update && \ - apt-get install -y --no-install-recommends \ - ca-certificates \ - curl \ - && rm -rf /var/lib/apt/lists/* - -# Create app user for security -RUN groupadd -r appuser && useradd -r -g appuser appuser - -# Copy the built binary from builder stage -COPY --from=builder /app/target/x86_64-unknown-linux-gnu/release/source-data-proxy /app/source-data-proxy - -# Set proper permissions -RUN chown appuser:appuser /app/source-data-proxy && \ - chmod +x /app/source-data-proxy - -# Switch to non-root user -USER appuser - -# Set working directory and expose port -WORKDIR /app -EXPOSE 8080 - -# Health check endpoint (using root path which returns version info) -HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ - CMD curl -f http://localhost:8080/ || exit 1 - -# Run the binary directly -ENTRYPOINT ["/app/source-data-proxy"] diff --git a/README.md b/README.md index f264726..4ef108f 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,111 @@ # Source Cooperative Data Proxy -This repository contains the rust application which hosts the Source Cooperative Data Proxy. +A read-only data proxy for [Source Cooperative](https://source.coop), built as a [Cloudflare Worker](https://developers.cloudflare.com/workers/) in Rust. It translates Source Cooperative URL paths into requests against cloud storage backends (S3, Azure Blob Storage, GCS) using the [multistore](https://github.com/developmentseed/multistore) S3 gateway. + +The proxy supports `GET`, `HEAD`, and S3-compatible `LIST` operations with anonymous access, and resolves storage backends dynamically via the Source Cooperative API. ## Getting Started ### Prerequisites -- Cargo installed on your local machine -- The AWS CLI installed on your local machine +- [Rust](https://rustup.rs/) with the `wasm32-unknown-unknown` target +- [wrangler](https://developers.cloudflare.com/workers/wrangler/) (Cloudflare Workers CLI) + +```sh +rustup target add wasm32-unknown-unknown +cargo install worker-build@0.7.5 +npm install -g wrangler@3 +``` ### Run Locally -To run the data proxy locally, run the following command: +```sh +wrangler dev +``` + +The proxy will be available at `http://localhost:8787`. + +### Running Tests + +Unit tests for routing and pagination run on native targets: + +```sh +cargo test +``` + +Integration tests run against a live instance: +```sh +wrangler dev & +python -m pytest tests/test_integration.py ``` -./scripts/run.sh + +### Example Requests + +```sh +# Version info +curl http://localhost:8787/ + +# List products for an account +curl "http://localhost:8787/cholmes?list-type=2&delimiter=/" + +# List files in a product +curl "http://localhost:8787/cholmes/admin-boundaries?list-type=2&prefix=&max-keys=10" + +# Get object metadata +curl -I http://localhost:8787/cholmes/admin-boundaries/countries.parquet + +# Download with range request +curl -r 0-1023 http://localhost:8787/cholmes/admin-boundaries/countries.parquet -o chunk.bin ``` + +## Architecture + +The proxy rewrites Source Cooperative URL paths (`/{account}/{product}/{key}`) into multistore's virtual bucket model, resolving storage backends dynamically via the Source Cooperative API. + +``` +Client Request: GET /{account}/{product}/{key} + │ + ├─ [routing] Parse path, rewrite to bucket={account}--{product}, key={key} + ├─ [registry] Resolve backend via Source API (product metadata + data connections) + ├─ [cache] Cache API responses (products: 5min, data connections: 30min, listings: 1min) + ├─ [multistore ProxyGateway] Generate presigned URL for the resolved storage backend + └─ Stream response back to client +``` + +### Modules + +| Module | Purpose | +|---|---| +| `src/lib.rs` | Fetch handler, request routing, CORS | +| `src/routing.rs` | Request classification and path rewriting | +| `src/registry.rs` | Source Cooperative API client and backend resolution | +| `src/cache.rs` | Cloudflare Cache API wrapper with per-datatype TTLs | +| `src/pagination.rs` | S3-compatible pagination for prefix listings | + +### Supported Operations + +| Operation | Description | +|---|---| +| `GET /` | Version info | +| `GET /{account}?list-type=2` | List products for an account | +| `GET /{account}/{product}?list-type=2` | List objects in a product (S3-compatible, with pagination) | +| `GET /{account}/{product}/{key}` | Download an object (supports range requests) | +| `HEAD /{account}/{product}/{key}` | Get object metadata | +| `OPTIONS *` | CORS preflight | + +Write operations (`PUT`, `POST`, `DELETE`, `PATCH`) return `405 Method Not Allowed`. + +## Configuration + +Environment variables (set in `wrangler.toml` or via Cloudflare dashboard): + +| Variable | Default | Description | +|---|---|---| +| `SOURCE_API_URL` | `https://source.coop` | Source Cooperative API base URL | +| `SOURCE_API_SECRET` | — | Optional API authentication token | +| `LOG_LEVEL` | `WARN` | Tracing level (`TRACE`, `DEBUG`, `INFO`, `WARN`, `ERROR`) | + +## Design Documents + +See [`docs/plans/`](docs/plans/) for architecture and design documents. diff --git a/deploy/.gitignore b/deploy/.gitignore deleted file mode 100644 index f60797b..0000000 --- a/deploy/.gitignore +++ /dev/null @@ -1,8 +0,0 @@ -*.js -!jest.config.js -*.d.ts -node_modules - -# CDK asset staging directory -.cdk.staging -cdk.out diff --git a/deploy/.npmignore b/deploy/.npmignore deleted file mode 100644 index c1d6d45..0000000 --- a/deploy/.npmignore +++ /dev/null @@ -1,6 +0,0 @@ -*.ts -!*.d.ts - -# CDK asset staging directory -.cdk.staging -cdk.out diff --git a/deploy/.nvmrc b/deploy/.nvmrc deleted file mode 100644 index 2bd5a0a..0000000 --- a/deploy/.nvmrc +++ /dev/null @@ -1 +0,0 @@ -22 diff --git a/deploy/README.md b/deploy/README.md deleted file mode 100644 index b88e456..0000000 --- a/deploy/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# Deployment - -This directory contains deployment tooling to create and manage the AWS infrastructure for the Source Data Proxy. - -It embraces an "Infrastructure as Code" approach via [AWS CDK](https://docs.aws.amazon.com/cdk/). Deployments should be triggered via Github Actions. diff --git a/deploy/bin/deploy.ts b/deploy/bin/deploy.ts deleted file mode 100644 index 5287996..0000000 --- a/deploy/bin/deploy.ts +++ /dev/null @@ -1,33 +0,0 @@ -#!/usr/bin/env node -import * as cdk from "aws-cdk-lib"; -import { DataProxyStack } from "../lib/data-proxy-stack"; -import { Tags } from "aws-cdk-lib"; - -const stage = process.env.STAGE || "dev"; -const vpcId = process.env.VPC_ID; -if (!vpcId) { - throw new Error("VPC_ID is not set"); -} -const certificateArn = process.env.CERTIFICATE_ARN; -if (!certificateArn) { - throw new Error("CERTIFICATE_ARN is not set"); -} -const taskCount = process.env.TASK_COUNT || 1; -const sourceApiUrl = process.env.SOURCE_API_URL || "https://s2.source.coop"; - -const app = new cdk.App(); -const stack = new DataProxyStack(app, `DataProxy-${stage}`, { - vpcId, - proxyDomain: `vercel-api-${stage}.internal`, - proxyDesiredCount: Number(taskCount), - sourceApiUrl, - env: { - account: process.env.AWS_ACCOUNT_ID, - region: process.env.AWS_REGION, - }, - certificateArn, -}); - -Tags.of(stack).add("Cfn-Stack", stack.stackName, { - applyToLaunchedInstances: true, -}); diff --git a/deploy/cdk.json b/deploy/cdk.json deleted file mode 100644 index 8dbbaa8..0000000 --- a/deploy/cdk.json +++ /dev/null @@ -1,99 +0,0 @@ -{ - "app": "npx ts-node --prefer-ts-exts bin/deploy.ts", - "watch": { - "include": [ - "**" - ], - "exclude": [ - "README.md", - "cdk*.json", - "**/*.d.ts", - "**/*.js", - "tsconfig.json", - "package*.json", - "yarn.lock", - "node_modules", - "test" - ] - }, - "context": { - "@aws-cdk/aws-lambda:recognizeLayerVersion": true, - "@aws-cdk/core:checkSecretUsage": true, - "@aws-cdk/core:target-partitions": [ - "aws", - "aws-cn" - ], - "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true, - "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true, - "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true, - "@aws-cdk/aws-iam:minimizePolicies": true, - "@aws-cdk/core:validateSnapshotRemovalPolicy": true, - "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true, - "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true, - "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true, - "@aws-cdk/aws-apigateway:disableCloudWatchRole": true, - "@aws-cdk/core:enablePartitionLiterals": true, - "@aws-cdk/aws-events:eventsTargetQueueSameAccount": true, - "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true, - "@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true, - "@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true, - "@aws-cdk/aws-route53-patters:useCertificate": true, - "@aws-cdk/customresources:installLatestAwsSdkDefault": false, - "@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true, - "@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true, - "@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true, - "@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true, - "@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true, - "@aws-cdk/aws-redshift:columnId": true, - "@aws-cdk/aws-stepfunctions-tasks:enableEmrServicePolicyV2": true, - "@aws-cdk/aws-ec2:restrictDefaultSecurityGroup": true, - "@aws-cdk/aws-apigateway:requestValidatorUniqueId": true, - "@aws-cdk/aws-kms:aliasNameRef": true, - "@aws-cdk/aws-kms:applyImportedAliasPermissionsToPrincipal": true, - "@aws-cdk/aws-autoscaling:generateLaunchTemplateInsteadOfLaunchConfig": true, - "@aws-cdk/core:includePrefixInUniqueNameGeneration": true, - "@aws-cdk/aws-efs:denyAnonymousAccess": true, - "@aws-cdk/aws-opensearchservice:enableOpensearchMultiAzWithStandby": true, - "@aws-cdk/aws-lambda-nodejs:useLatestRuntimeVersion": true, - "@aws-cdk/aws-efs:mountTargetOrderInsensitiveLogicalId": true, - "@aws-cdk/aws-rds:auroraClusterChangeScopeOfInstanceParameterGroupWithEachParameters": true, - "@aws-cdk/aws-appsync:useArnForSourceApiAssociationIdentifier": true, - "@aws-cdk/aws-rds:preventRenderingDeprecatedCredentials": true, - "@aws-cdk/aws-codepipeline-actions:useNewDefaultBranchForCodeCommitSource": true, - "@aws-cdk/aws-cloudwatch-actions:changeLambdaPermissionLogicalIdForLambdaAction": true, - "@aws-cdk/aws-codepipeline:crossAccountKeysDefaultValueToFalse": true, - "@aws-cdk/aws-codepipeline:defaultPipelineTypeToV2": true, - "@aws-cdk/aws-kms:reduceCrossAccountRegionPolicyScope": true, - "@aws-cdk/aws-eks:nodegroupNameAttribute": true, - "@aws-cdk/aws-ec2:ebsDefaultGp3Volume": true, - "@aws-cdk/aws-ecs:removeDefaultDeploymentAlarm": true, - "@aws-cdk/custom-resources:logApiResponseDataPropertyTrueDefault": false, - "@aws-cdk/aws-s3:keepNotificationInImportedBucket": false, - "@aws-cdk/core:explicitStackTags": true, - "@aws-cdk/aws-ecs:enableImdsBlockingDeprecatedFeature": false, - "@aws-cdk/aws-ecs:disableEcsImdsBlocking": true, - "@aws-cdk/aws-ecs:reduceEc2FargateCloudWatchPermissions": true, - "@aws-cdk/aws-dynamodb:resourcePolicyPerReplica": true, - "@aws-cdk/aws-ec2:ec2SumTImeoutEnabled": true, - "@aws-cdk/aws-appsync:appSyncGraphQLAPIScopeLambdaPermission": true, - "@aws-cdk/aws-rds:setCorrectValueForDatabaseInstanceReadReplicaInstanceResourceId": true, - "@aws-cdk/core:cfnIncludeRejectComplexResourceUpdateCreatePolicyIntrinsics": true, - "@aws-cdk/aws-lambda-nodejs:sdkV3ExcludeSmithyPackages": true, - "@aws-cdk/aws-stepfunctions-tasks:fixRunEcsTaskPolicy": true, - "@aws-cdk/aws-ec2:bastionHostUseAmazonLinux2023ByDefault": true, - "@aws-cdk/aws-route53-targets:userPoolDomainNameMethodWithoutCustomResource": true, - "@aws-cdk/aws-elasticloadbalancingV2:albDualstackWithoutPublicIpv4SecurityGroupRulesDefault": true, - "@aws-cdk/aws-iam:oidcRejectUnauthorizedConnections": true, - "@aws-cdk/core:enableAdditionalMetadataCollection": true, - "@aws-cdk/aws-lambda:createNewPoliciesWithAddToRolePolicy": false, - "@aws-cdk/aws-s3:setUniqueReplicationRoleName": true, - "@aws-cdk/aws-events:requireEventBusPolicySid": true, - "@aws-cdk/core:aspectPrioritiesMutating": true, - "@aws-cdk/aws-dynamodb:retainTableReplica": true, - "@aws-cdk/aws-stepfunctions:useDistributedMapResultWriterV2": true, - "@aws-cdk/s3-notifications:addS3TrustKeyPolicyForSnsSubscriptions": true, - "@aws-cdk/aws-ec2:requirePrivateSubnetsForEgressOnlyInternetGateway": true, - "@aws-cdk/aws-s3:publicAccessBlockedByDefault": true, - "@aws-cdk/aws-lambda:useCdkManagedLogGroup": true - } -} diff --git a/deploy/lib/data-proxy-stack.ts b/deploy/lib/data-proxy-stack.ts deleted file mode 100644 index 180cd94..0000000 --- a/deploy/lib/data-proxy-stack.ts +++ /dev/null @@ -1,38 +0,0 @@ -import * as cdk from "aws-cdk-lib"; -import { aws_ec2 as ec2 } from "aws-cdk-lib"; -import { Construct } from "constructs"; -import { VercelApiProxy } from "./vercel-api-proxy"; -import { SourceDataProxy } from "./source-data-proxy"; - -interface DataProxyStackProps extends cdk.StackProps { - vpcId: string; - proxyDomain: string; - sourceApiUrl: string; - proxyDesiredCount: number; - certificateArn: string; -} - -export class DataProxyStack extends cdk.Stack { - constructor(scope: Construct, id: string, props: DataProxyStackProps) { - super(scope, id, props); - - const vpc = ec2.Vpc.fromLookup(this, "vpc", { vpcId: props.vpcId }); - - // Create Vercel API proxy (existing functionality) - const vercelApiProxy = new VercelApiProxy(this, "vercel-api-proxy", { - vpc, - proxyDomain: props.proxyDomain, - }); - - new SourceDataProxy(this, "source-data-proxy", { - vpc, - environment: { - RUST_LOG: "info", - SOURCE_API_PROXY_URL: vercelApiProxy.url, - SOURCE_API_URL: props.sourceApiUrl, - }, - desiredCount: props.proxyDesiredCount, - certificateArn: props.certificateArn, - }); - } -} diff --git a/deploy/lib/source-data-proxy.ts b/deploy/lib/source-data-proxy.ts deleted file mode 100644 index b739585..0000000 --- a/deploy/lib/source-data-proxy.ts +++ /dev/null @@ -1,127 +0,0 @@ -import * as cdk from "aws-cdk-lib"; -import { - aws_ec2 as ec2, - aws_ecs as ecs, - aws_ecs_patterns as ecs_patterns, - aws_logs as logs, - aws_secretsmanager as secretsmanager, - aws_elasticloadbalancingv2 as elbv2, -} from "aws-cdk-lib"; -import { Certificate } from "aws-cdk-lib/aws-certificatemanager"; -import { Construct } from "constructs"; - -interface SourceDataProxyProps { - vpc: ec2.IVpc; - desiredCount: number; - environment: Record; - certificateArn: string; -} - -export class SourceDataProxy extends Construct { - public readonly service: ecs_patterns.ApplicationLoadBalancedFargateService; - - constructor(scope: Construct, id: string, props: SourceDataProxyProps) { - super(scope, id); - - const stack = cdk.Stack.of(this); - - const cluster = new ecs.Cluster(this, "cluster", { - clusterName: `${stack.stackName}-cluster`, - vpc: props.vpc, - enableFargateCapacityProviders: true, - containerInsightsV2: ecs.ContainerInsights.ENHANCED, - }); - - const sourceApiKeySecret = new secretsmanager.Secret( - this, - "source-api-key", - { - secretName: `${stack.stackName}-source-api-key`, - description: - "API Key used to make authenticated requests to the Source API on Vercel", - } - ); - - // Create Application Load Balanced Fargate Service using the pattern - this.service = new ecs_patterns.ApplicationLoadBalancedFargateService( - this, - "service", - { - serviceName: `${stack.stackName}-proxy`, - cluster, - cpu: 4 * 1024, // 4 vCPU - desiredCount: props.desiredCount, - memoryLimitMiB: 12 * 1024, // 12 GB - taskImageOptions: { - image: ecs.ContainerImage.fromAsset("../", { - buildArgs: { - BUILDPLATFORM: "linux/amd64", - TARGETPLATFORM: "linux/amd64", - }, - }), - containerPort: 8080, - family: `${stack.stackName}-proxy`, - environment: props.environment, - secrets: { - SOURCE_API_KEY: ecs.Secret.fromSecretsManager(sourceApiKeySecret), - }, - logDriver: ecs.LogDrivers.awsLogs({ - streamPrefix: "ecs", - logGroup: new logs.LogGroup(this, "log-group", { - logGroupName: `/ecs/${stack.stackName}-proxy`, - retention: logs.RetentionDays.ONE_MONTH, - }), - mode: ecs.AwsLogDriverMode.NON_BLOCKING, - maxBufferSize: cdk.Size.mebibytes(25), - }), - }, - runtimePlatform: { - cpuArchitecture: ecs.CpuArchitecture.X86_64, - operatingSystemFamily: ecs.OperatingSystemFamily.LINUX, - }, - publicLoadBalancer: true, - loadBalancerName: `${stack.stackName}-alb`, - protocol: elbv2.ApplicationProtocol.HTTPS, - listenerPort: 443, - certificate: Certificate.fromCertificateArn( - this, - "certificate", - props.certificateArn - ), - enableExecuteCommand: true, - circuitBreaker: { rollback: true }, - assignPublicIp: true, - capacityProviderStrategies: [ - { - capacityProvider: "FARGATE_SPOT", - // Prefer spot instances over on-demand instances - weight: 2, - }, - { - capacityProvider: "FARGATE", - // Use on-demand instances as a fallback - weight: 1, - }, - ], - } - ); - - if (this.service.taskDefinition.executionRole) { - sourceApiKeySecret.grantRead(this.service.taskDefinition.executionRole); - } - - // Output the ALB DNS name - new cdk.CfnOutput(this, "alb-dns", { - value: this.service.loadBalancer.loadBalancerDnsName, - description: "Application Load Balancer DNS name", - exportName: `${cdk.Stack.of(this).stackName}-alb-dns`, - }); - - // Output the service name - new cdk.CfnOutput(this, "service-name", { - value: this.service.service.serviceName, - description: "ECS Service name", - exportName: `${cdk.Stack.of(this).stackName}-service-name`, - }); - } -} diff --git a/deploy/lib/vercel-api-proxy.ts b/deploy/lib/vercel-api-proxy.ts deleted file mode 100644 index d8e22e6..0000000 --- a/deploy/lib/vercel-api-proxy.ts +++ /dev/null @@ -1,113 +0,0 @@ -import * as cdk from "aws-cdk-lib"; -import { - aws_ec2 as ec2, - aws_iam as iam, - aws_route53 as route53, - aws_route53_targets as route53_targets, -} from "aws-cdk-lib"; -import { Construct } from "constructs"; - -interface VercelApiProxyProps { - vpc: ec2.IVpc; - proxyDomain: string; -} - -export class VercelApiProxy extends Construct { - public readonly url: string; - /** - * To work around Vercel's firewall, we must proxy all requests for the Proxy API through - * a Squid proxy. This will allow us to have a stable IP address for the Proxy API which - * we can add to the Vercel firewall's bypass list. This allows us to retain ephemeral IP - * addresses for the Proxy API and to avoid using other techniques like passing data - * through a NAT Gateway which would have considerable cost implications. - */ - constructor(scope: Construct, id: string, props: VercelApiProxyProps) { - super(scope, id); - - const proxyPort = 3128; - - // Create security group for the proxy - const proxySg = new ec2.SecurityGroup(this, "proxy-sg", { - vpc: props.vpc, - description: "Allow inbound from ECS for Squid proxy", - allowAllOutbound: true, - }); - - // Allow ECS (internal) traffic on port 3128 - proxySg.addIngressRule( - ec2.Peer.ipv4(props.vpc.vpcCidrBlock), - ec2.Port.tcp(proxyPort), - "Allow ECS to connect to Squid" - ); - - // Squid install and minimal config - const userData = ec2.UserData.forLinux(); - userData.addCommands( - "yum update -y", - "yum install -y squid", - - // Write squid.conf using heredoc - "cat <<'EOF' > /etc/squid/squid.conf", - `http_port ${proxyPort}`, - "acl all src 0.0.0.0/0", - "http_access allow all", - "EOF", - - // Enable and start Squid - "systemctl enable squid", - "systemctl restart squid" - ); - - // Enable SSM access for the EC2 instance - const ssmRole = new iam.Role(this, "ec2-ssm-role", { - assumedBy: new iam.ServicePrincipal("ec2.amazonaws.com"), - managedPolicies: [ - iam.ManagedPolicy.fromAwsManagedPolicyName( - "AmazonSSMManagedInstanceCore" - ), - ], - }); - - // Launch EC2 instance - const instance = new ec2.Instance(this, "squid-proxy", { - vpc: props.vpc, - role: ssmRole, - instanceType: ec2.InstanceType.of( - ec2.InstanceClass.T3, - ec2.InstanceSize.MICRO - ), - machineImage: ec2.MachineImage.latestAmazonLinux2023(), - vpcSubnets: { subnetType: ec2.SubnetType.PUBLIC }, - securityGroup: proxySg, - userData, - }); - - // Allocate and associate Elastic IP - const eip = new ec2.CfnEIP(this, "proxy-eip", { - domain: "vpc", - tags: [ - { - key: "Name", - value: `${cdk.Stack.of(this).stackName}-proxy-eip`, - }, - ], - }); - new ec2.CfnEIPAssociation(this, "proxy-eip-assoc", { - allocationId: eip.attrAllocationId, - instanceId: instance.instanceId, - }); - - // Route 53 Private Hosted Zone - const zone = new route53.PrivateHostedZone(this, "proxy-zone", { - vpc: props.vpc, - zoneName: props.proxyDomain, - }); - new route53.ARecord(this, "proxy-a-record", { - zone, - target: route53.RecordTarget.fromIpAddresses(instance.instancePrivateIp), - ttl: cdk.Duration.seconds(60), - }); - - this.url = `http://${props.proxyDomain}:${proxyPort}`; - } -} diff --git a/deploy/package-lock.json b/deploy/package-lock.json deleted file mode 100644 index 788c7e3..0000000 --- a/deploy/package-lock.json +++ /dev/null @@ -1,678 +0,0 @@ -{ - "name": "deploy", - "version": "0.1.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "deploy", - "version": "0.1.0", - "dependencies": { - "aws-cdk-lib": "2.206.0", - "constructs": "^10.0.0" - }, - "bin": { - "deploy": "bin/deploy.js" - }, - "devDependencies": { - "@types/node": "22.7.9", - "aws-cdk": "2.1023.0", - "ts-node": "^10.9.2", - "typescript": "~5.6.3" - } - }, - "node_modules/@aws-cdk/asset-awscli-v1": { - "version": "2.2.242", - "resolved": "https://registry.npmjs.org/@aws-cdk/asset-awscli-v1/-/asset-awscli-v1-2.2.242.tgz", - "integrity": "sha512-4c1bAy2ISzcdKXYS1k4HYZsNrgiwbiDzj36ybwFVxEWZXVAP0dimQTCaB9fxu7sWzEjw3d+eaw6Fon+QTfTIpQ==", - "license": "Apache-2.0" - }, - "node_modules/@aws-cdk/asset-node-proxy-agent-v6": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@aws-cdk/asset-node-proxy-agent-v6/-/asset-node-proxy-agent-v6-2.1.0.tgz", - "integrity": "sha512-7bY3J8GCVxLupn/kNmpPc5VJz8grx+4RKfnnJiO1LG+uxkZfANZG3RMHhE+qQxxwkyQ9/MfPtTpf748UhR425A==", - "license": "Apache-2.0" - }, - "node_modules/@aws-cdk/cloud-assembly-schema": { - "version": "45.2.0", - "resolved": "https://registry.npmjs.org/@aws-cdk/cloud-assembly-schema/-/cloud-assembly-schema-45.2.0.tgz", - "integrity": "sha512-5TTUkGHQ+nfuUGwKA8/Yraxb+JdNUh4np24qk/VHXmrCMq+M6HfmGWfhcg/QlHA2S5P3YIamfYHdQAB4uSNLAg==", - "bundleDependencies": [ - "jsonschema", - "semver" - ], - "license": "Apache-2.0", - "dependencies": { - "jsonschema": "~1.4.1", - "semver": "^7.7.2" - }, - "engines": { - "node": ">= 18.0.0" - } - }, - "node_modules/@aws-cdk/cloud-assembly-schema/node_modules/jsonschema": { - "version": "1.4.1", - "inBundle": true, - "license": "MIT", - "engines": { - "node": "*" - } - }, - "node_modules/@aws-cdk/cloud-assembly-schema/node_modules/semver": { - "version": "7.7.2", - "inBundle": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@cspotcode/source-map-support": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", - "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/trace-mapping": "0.3.9" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", - "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.0.3", - "@jridgewell/sourcemap-codec": "^1.4.10" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz", - "integrity": "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@tsconfig/node10": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", - "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@tsconfig/node12": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", - "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", - "dev": true, - "license": "MIT" - }, - "node_modules/@tsconfig/node14": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", - "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", - "dev": true, - "license": "MIT" - }, - "node_modules/@tsconfig/node16": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", - "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/node": { - "version": "22.7.9", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.9.tgz", - "integrity": "sha512-jrTfRC7FM6nChvU7X2KqcrgquofrWLFDeYC1hKfwNWomVvrn7JIksqf344WN2X/y8xrgqBd2dJATZV4GbatBfg==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.19.2" - } - }, - "node_modules/acorn": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", - "dev": true, - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-walk": { - "version": "8.3.4", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", - "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "acorn": "^8.11.0" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/arg": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", - "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", - "dev": true, - "license": "MIT" - }, - "node_modules/aws-cdk": { - "version": "2.1023.0", - "resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.1023.0.tgz", - "integrity": "sha512-DWMA+IrAsBUNF2RvH7ujpDp7wSJkqTkRL8yfK4AYpEjoGY1KMaKIfxz3M3+Nk3ogM7VhZiW3OGWEOgyDF47HOQ==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "cdk": "bin/cdk" - }, - "engines": { - "node": ">= 18.0.0" - }, - "optionalDependencies": { - "fsevents": "2.3.2" - } - }, - "node_modules/aws-cdk-lib": { - "version": "2.206.0", - "resolved": "https://registry.npmjs.org/aws-cdk-lib/-/aws-cdk-lib-2.206.0.tgz", - "integrity": "sha512-WQGSSzSX+CvIG3j4GICxCAARGaB2dbB2ZiAn8dqqWdUkF6G9pedlSd3bjB0NHOqrxJMu3jYQCYf3gLYTaJuR8A==", - "bundleDependencies": [ - "@balena/dockerignore", - "case", - "fs-extra", - "ignore", - "jsonschema", - "minimatch", - "punycode", - "semver", - "table", - "yaml", - "mime-types" - ], - "license": "Apache-2.0", - "dependencies": { - "@aws-cdk/asset-awscli-v1": "2.2.242", - "@aws-cdk/asset-node-proxy-agent-v6": "^2.1.0", - "@aws-cdk/cloud-assembly-schema": "^45.0.0", - "@balena/dockerignore": "^1.0.2", - "case": "1.6.3", - "fs-extra": "^11.3.0", - "ignore": "^5.3.2", - "jsonschema": "^1.5.0", - "mime-types": "^2.1.35", - "minimatch": "^3.1.2", - "punycode": "^2.3.1", - "semver": "^7.7.2", - "table": "^6.9.0", - "yaml": "1.10.2" - }, - "engines": { - "node": ">= 14.15.0" - }, - "peerDependencies": { - "constructs": "^10.0.0" - } - }, - "node_modules/aws-cdk-lib/node_modules/@balena/dockerignore": { - "version": "1.0.2", - "inBundle": true, - "license": "Apache-2.0" - }, - "node_modules/aws-cdk-lib/node_modules/ajv": { - "version": "8.17.1", - "inBundle": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/aws-cdk-lib/node_modules/ansi-regex": { - "version": "5.0.1", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/aws-cdk-lib/node_modules/ansi-styles": { - "version": "4.3.0", - "inBundle": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/aws-cdk-lib/node_modules/astral-regex": { - "version": "2.0.0", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/aws-cdk-lib/node_modules/balanced-match": { - "version": "1.0.2", - "inBundle": true, - "license": "MIT" - }, - "node_modules/aws-cdk-lib/node_modules/brace-expansion": { - "version": "1.1.12", - "inBundle": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/aws-cdk-lib/node_modules/case": { - "version": "1.6.3", - "inBundle": true, - "license": "(MIT OR GPL-3.0-or-later)", - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/aws-cdk-lib/node_modules/color-convert": { - "version": "2.0.1", - "inBundle": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/aws-cdk-lib/node_modules/color-name": { - "version": "1.1.4", - "inBundle": true, - "license": "MIT" - }, - "node_modules/aws-cdk-lib/node_modules/concat-map": { - "version": "0.0.1", - "inBundle": true, - "license": "MIT" - }, - "node_modules/aws-cdk-lib/node_modules/emoji-regex": { - "version": "8.0.0", - "inBundle": true, - "license": "MIT" - }, - "node_modules/aws-cdk-lib/node_modules/fast-deep-equal": { - "version": "3.1.3", - "inBundle": true, - "license": "MIT" - }, - "node_modules/aws-cdk-lib/node_modules/fast-uri": { - "version": "3.0.6", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "inBundle": true, - "license": "BSD-3-Clause" - }, - "node_modules/aws-cdk-lib/node_modules/fs-extra": { - "version": "11.3.0", - "inBundle": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=14.14" - } - }, - "node_modules/aws-cdk-lib/node_modules/graceful-fs": { - "version": "4.2.11", - "inBundle": true, - "license": "ISC" - }, - "node_modules/aws-cdk-lib/node_modules/ignore": { - "version": "5.3.2", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/aws-cdk-lib/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/aws-cdk-lib/node_modules/json-schema-traverse": { - "version": "1.0.0", - "inBundle": true, - "license": "MIT" - }, - "node_modules/aws-cdk-lib/node_modules/jsonfile": { - "version": "6.1.0", - "inBundle": true, - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/aws-cdk-lib/node_modules/jsonschema": { - "version": "1.5.0", - "inBundle": true, - "license": "MIT", - "engines": { - "node": "*" - } - }, - "node_modules/aws-cdk-lib/node_modules/lodash.truncate": { - "version": "4.4.2", - "inBundle": true, - "license": "MIT" - }, - "node_modules/aws-cdk-lib/node_modules/mime-db": { - "version": "1.52.0", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/aws-cdk-lib/node_modules/mime-types": { - "version": "2.1.35", - "inBundle": true, - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/aws-cdk-lib/node_modules/minimatch": { - "version": "3.1.2", - "inBundle": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/aws-cdk-lib/node_modules/punycode": { - "version": "2.3.1", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/aws-cdk-lib/node_modules/require-from-string": { - "version": "2.0.2", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/aws-cdk-lib/node_modules/semver": { - "version": "7.7.2", - "inBundle": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/aws-cdk-lib/node_modules/slice-ansi": { - "version": "4.0.0", - "inBundle": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "astral-regex": "^2.0.0", - "is-fullwidth-code-point": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/slice-ansi?sponsor=1" - } - }, - "node_modules/aws-cdk-lib/node_modules/string-width": { - "version": "4.2.3", - "inBundle": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/aws-cdk-lib/node_modules/strip-ansi": { - "version": "6.0.1", - "inBundle": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/aws-cdk-lib/node_modules/table": { - "version": "6.9.0", - "inBundle": true, - "license": "BSD-3-Clause", - "dependencies": { - "ajv": "^8.0.1", - "lodash.truncate": "^4.4.2", - "slice-ansi": "^4.0.0", - "string-width": "^4.2.3", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/aws-cdk-lib/node_modules/universalify": { - "version": "2.0.1", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/aws-cdk-lib/node_modules/yaml": { - "version": "1.10.2", - "inBundle": true, - "license": "ISC", - "engines": { - "node": ">= 6" - } - }, - "node_modules/constructs": { - "version": "10.4.2", - "resolved": "https://registry.npmjs.org/constructs/-/constructs-10.4.2.tgz", - "integrity": "sha512-wsNxBlAott2qg8Zv87q3eYZYgheb9lchtBfjHzzLHtXbttwSrHPs1NNQbBrmbb1YZvYg2+Vh0Dor76w4mFxJkA==", - "license": "Apache-2.0" - }, - "node_modules/create-require": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", - "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/diff": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", - "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.3.1" - } - }, - "node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/make-error": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", - "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", - "dev": true, - "license": "ISC" - }, - "node_modules/ts-node": { - "version": "10.9.2", - "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", - "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@cspotcode/source-map-support": "^0.8.0", - "@tsconfig/node10": "^1.0.7", - "@tsconfig/node12": "^1.0.7", - "@tsconfig/node14": "^1.0.0", - "@tsconfig/node16": "^1.0.2", - "acorn": "^8.4.1", - "acorn-walk": "^8.1.1", - "arg": "^4.1.0", - "create-require": "^1.1.0", - "diff": "^4.0.1", - "make-error": "^1.1.1", - "v8-compile-cache-lib": "^3.0.1", - "yn": "3.1.1" - }, - "bin": { - "ts-node": "dist/bin.js", - "ts-node-cwd": "dist/bin-cwd.js", - "ts-node-esm": "dist/bin-esm.js", - "ts-node-script": "dist/bin-script.js", - "ts-node-transpile-only": "dist/bin-transpile.js", - "ts-script": "dist/bin-script-deprecated.js" - }, - "peerDependencies": { - "@swc/core": ">=1.2.50", - "@swc/wasm": ">=1.2.50", - "@types/node": "*", - "typescript": ">=2.7" - }, - "peerDependenciesMeta": { - "@swc/core": { - "optional": true - }, - "@swc/wasm": { - "optional": true - } - } - }, - "node_modules/typescript": { - "version": "5.6.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", - "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/undici-types": { - "version": "6.19.8", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", - "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", - "dev": true, - "license": "MIT" - }, - "node_modules/v8-compile-cache-lib": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", - "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", - "dev": true, - "license": "MIT" - }, - "node_modules/yn": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", - "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - } - } -} diff --git a/deploy/package.json b/deploy/package.json deleted file mode 100644 index 2b902bc..0000000 --- a/deploy/package.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "deploy", - "version": "0.1.0", - "bin": { - "deploy": "bin/deploy.js" - }, - "scripts": { - "build": "tsc", - "watch": "tsc -w", - "cdk": "cdk" - }, - "devDependencies": { - "@types/node": "22.7.9", - "aws-cdk": "2.1023.0", - "ts-node": "^10.9.2", - "typescript": "~5.6.3" - }, - "dependencies": { - "aws-cdk-lib": "2.206.0", - "constructs": "^10.0.0" - } -} diff --git a/deploy/tsconfig.json b/deploy/tsconfig.json deleted file mode 100644 index 28bb557..0000000 --- a/deploy/tsconfig.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2022", - "module": "NodeNext", - "moduleResolution": "NodeNext", - "lib": [ - "es2022" - ], - "declaration": true, - "strict": true, - "noImplicitAny": true, - "strictNullChecks": true, - "noImplicitThis": true, - "alwaysStrict": true, - "noUnusedLocals": false, - "noUnusedParameters": false, - "noImplicitReturns": true, - "noFallthroughCasesInSwitch": false, - "inlineSourceMap": true, - "inlineSources": true, - "experimentalDecorators": true, - "strictPropertyInitialization": false, - "typeRoots": [ - "./node_modules/@types" - ] - }, - "exclude": [ - "node_modules", - "cdk.out" - ] -} diff --git a/rust-toolchain.toml b/rust-toolchain.toml deleted file mode 100644 index 292fe49..0000000 --- a/rust-toolchain.toml +++ /dev/null @@ -1,2 +0,0 @@ -[toolchain] -channel = "stable" diff --git a/scripts/build-push.sh b/scripts/build-push.sh deleted file mode 100755 index 92f28a2..0000000 --- a/scripts/build-push.sh +++ /dev/null @@ -1,4 +0,0 @@ -VERSION=$(cargo metadata --format-version=1 --no-deps | jq -r '.packages[0].version') -docker buildx build --platform linux/arm64 -t 417712557820.dkr.ecr.us-west-2.amazonaws.com/source-data-proxy:v$VERSION --push . -aws ecr get-login-password --region us-west-2 --profile opendata | docker login --username AWS --password-stdin 417712557820.dkr.ecr.us-west-2.amazonaws.com -docker push 417712557820.dkr.ecr.us-west-2.amazonaws.com/source-data-proxy:v$VERSION diff --git a/scripts/deploy.sh b/scripts/deploy.sh deleted file mode 100755 index c8fc6d8..0000000 --- a/scripts/deploy.sh +++ /dev/null @@ -1,47 +0,0 @@ -VERSION=$(git tag --points-at HEAD) -SOURCE_API_URL="https://source.coop" - -# Check if the current commit is a release commit -if [ -z "$VERSION" ]; then - echo "No release tag found for this commit. Are you sure you checked out a release commit?" - exit 1; -fi - -# Check if the image for the current version exists in ECR -if [ -z "$(aws ecr describe-images --repository-name source-data-proxy --image-ids=imageTag=$VERSION --profile opendata 2> /dev/null)" ]; then - echo "Could not find image for version $VERSION in ECR. Did you build and push the image?" - exit 1; -fi - -if [ -z "${SOURCE_KEY}" ]; then - echo "The SOURCE_KEY environment variable is not set" - exit 1; -fi - -echo "Deploying $VERSION..." - -jq --arg api_url "$SOURCE_API_URL" --arg image "417712557820.dkr.ecr.us-west-2.amazonaws.com/source-data-proxy:$VERSION" --arg source_key "$SOURCE_KEY" '(.containerDefinitions[0].environment |= [{"name":"SOURCE_KEY", "value": $source_key},{"name":"SOURCE_API_URL", "value": $api_url}]) | (.containerDefinitions[0].image |= $image)' scripts/task_definition.json > scripts/task_definition_deploy.json - -# Register the task definition -if [ -z "$(aws ecs register-task-definition --cli-input-json "file://scripts/task_definition_deploy.json" --profile opendata --no-cli-auto-prompt 2> /dev/null)" ]; then - echo "Failed to create task definition" - echo "Cleaning Up..." - rm scripts/task_definition_deploy.json - exit 1; -fi - -echo "Created Task Definition" - -TASK_DEFINITION_ARN=$(aws ecs list-task-definitions --family-prefix source-data-proxy --status ACTIVE --profile opendata --query "taskDefinitionArns[-1]" --output text) - -echo "Updating Service..." - -if [ -z "$(aws ecs update-service --cluster SourceCooperative-Prod --service source-data-proxy --task-definition $TASK_DEFINITION_ARN --profile opendata 2> /dev/null)" ]; then - echo "Failed to update service" - echo "Cleaning Up..." - rm scripts/task_definition_deploy.json - exit 1; -fi - -echo "Cleaning Up..." -rm scripts/task_definition_deploy.json diff --git a/scripts/run.sh b/scripts/run.sh deleted file mode 100755 index 71b1457..0000000 --- a/scripts/run.sh +++ /dev/null @@ -1,3 +0,0 @@ -export SOURCE_KEY=foobar -export SOURCE_API_URL=http://localhost:3000 -cargo run diff --git a/scripts/tag-release.sh b/scripts/tag-release.sh deleted file mode 100755 index 84e76f2..0000000 --- a/scripts/tag-release.sh +++ /dev/null @@ -1,29 +0,0 @@ -if [[ $(git status -s) ]]; then - echo "ERROR: Please commit all of your changes before tagging the release." - exit 1 -fi - -echo "What type of bump would you like to do?" -echo "1) Patch" -echo "2) Minor" -echo "3) Major" - -read BUMP_TYPE - -if [ $BUMP_TYPE -eq 1 ]; then - cargo bump patch -elif [ $BUMP_TYPE -eq 2 ]; then - cargo bump minor -elif [ $BUMP_TYPE -eq 3 ]; then - cargo bump major -else - echo "ERROR: Invalid bump type" - exit 1 -fi - -VERSION=$(cargo metadata --format-version=1 --no-deps | jq -r '.packages[0].version') -git add Cargo.toml -git add Cargo.lock -git commit -m "Bump version to v$VERSION" -git tag -a "v$VERSION" -m "v$VERSION" -git push origin --tags diff --git a/scripts/task_definition.json b/scripts/task_definition.json deleted file mode 100644 index 6373faf..0000000 --- a/scripts/task_definition.json +++ /dev/null @@ -1,57 +0,0 @@ -{ - "family": "source-data-proxy", - "containerDefinitions": [ - { - "name": "source-data-proxy", - "image": "", - "cpu": 0, - "portMappings": [ - { - "name": "webserver", - "containerPort": 8080, - "hostPort": 8080, - "protocol": "tcp", - "appProtocol": "http" - } - ], - "essential": true, - "environment": [ - { - "name": "SOURCE_KEY", - "value": "{SOURCE_KEY_VALUE_HERE}" - }, - { - "name": "SOURCE_API_URL", - "value": "{SOURCE_API_URL}" - } - ], - "environmentFiles": [], - "mountPoints": [], - "volumesFrom": [], - "ulimits": [], - "logConfiguration": { - "logDriver": "awslogs", - "options": { - "awslogs-group": "/ecs/Source-Data-Proxy", - "mode": "non-blocking", - "awslogs-create-group": "true", - "max-buffer-size": "25m", - "awslogs-region": "us-west-2", - "awslogs-stream-prefix": "ecs" - }, - "secretOptions": [] - }, - "systemControls": [] - } - ], - "taskRoleArn": "arn:aws:iam::417712557820:role/SourceCooperative", - "executionRoleArn": "arn:aws:iam::417712557820:role/ecsTaskExecutionRole", - "networkMode": "awsvpc", - "requiresCompatibilities": ["FARGATE"], - "cpu": "4096", - "memory": "12288", - "runtimePlatform": { - "cpuArchitecture": "ARM64", - "operatingSystemFamily": "LINUX" - } -} diff --git a/src/apis/mod.rs b/src/apis/mod.rs deleted file mode 100644 index 0867ca7..0000000 --- a/src/apis/mod.rs +++ /dev/null @@ -1,31 +0,0 @@ -pub mod source; - -use crate::{backends::common::Repository, utils::auth::UserIdentity, utils::errors::BackendError}; -use async_trait::async_trait; - -pub struct Account { - pub repositories: Vec, -} - -impl Account { - fn default() -> Account { - Account { - repositories: Vec::new(), - } - } -} - -#[async_trait] -pub trait Api { - async fn get_backend_client( - &self, - account_id: &str, - repository_id: &str, - ) -> Result, BackendError>; - - async fn get_account( - &self, - account_id: String, - user_identity: UserIdentity, - ) -> Result; -} diff --git a/src/apis/source/mod.rs b/src/apis/source/mod.rs deleted file mode 100644 index f222bc8..0000000 --- a/src/apis/source/mod.rs +++ /dev/null @@ -1,689 +0,0 @@ -//! Source API client and data structures for the Source Cooperative platform. -//! -//! This module provides types and functionality for interacting with the Source API, -//! including product management, account handling, and storage backend integration. -//! -//! # Overview -//! -//! The Source Cooperative is a platform for sharing and collaborating on data products. -//! This module defines the core data structures that represent products, accounts, -//! and their associated metadata in the system. -//! -//! # Key Types -//! -//! - [`SourceProduct`] - Main product entity with metadata and configuration -//! - [`SourceProductAccount`] - Account information for product owners -//! - [`SourceProductMetadata`] - Product configuration including mirrors and roles -//! - [`SourceApi`] - API client for interacting with the Source platform -//! -//! # Examples -//! -//! ## Creating a Source API client -//! -//! ```rust -//! use source_data_proxy::apis::source::SourceApi; -//! -//! let api = SourceApi::new( -//! "https://api.source.coop".to_string(), -//! "your-api-key".to_string(), -//! None -//! ); -//! ``` -//! -//! ## Parsing product data from JSON -//! -//! ```rust -//! use serde_json; -//! use source_data_proxy::apis::source::SourceProduct; -//! -//! let json = r#"{ -//! "product_id": "example-product", -//! "account_id": "example-account", -//! "title": "Example Product", -//! "description": "An example product", -//! "created_at": "2023-01-01T00:00:00Z", -//! "updated_at": "2023-01-01T00:00:00Z", -//! "visibility": "public", -//! "disabled": false, -//! "data_mode": "open", -//! "featured": 0, -//! "metadata": { ... }, -//! "account": { ... } -//! }"#; -//! -//! let product: SourceProduct = serde_json::from_str(json)?; -//! ``` - -mod types; - -// Re-export all types -pub use types::*; - -use super::{Account, Api}; -use crate::backends::azure::AzureRepository; -use crate::backends::common::Repository; -use crate::backends::s3::S3Repository; -use crate::utils::api::process_json_response; -use crate::utils::auth::UserIdentity; -use crate::utils::errors::BackendError; -use async_trait::async_trait; -use moka::future::Cache; -use rusoto_core::Region; -use std::sync::Arc; -use std::time::Duration; - -/// Client for interacting with the Source Cooperative API. -/// -/// The `SourceApi` provides methods for managing products, accounts, and storage -/// backends. It includes built-in caching for improved performance and supports -/// both direct API calls and proxy-based requests. -/// -/// # Features -/// -/// - **Caching**: Built-in caching for products, data connections, and permissions -/// - **Multiple Storage Backends**: Support for S3, Azure, GCS, MinIO, and Ceph -/// - **Proxy Support**: Optional proxy configuration for network requests -/// - **Authentication**: API key-based authentication with user identity support -/// -/// # Examples -/// -/// ```rust -/// use source_data_proxy::apis::source::SourceApi; -/// -/// let api = SourceApi::new( -/// "https://api.source.coop".to_string(), -/// "your-api-key".to_string(), -/// None // No proxy -/// ); -/// -/// // Get a product -/// let product = api.get_repository_record("account-id", "product-id").await?; -/// ``` -#[derive(Clone)] -pub struct SourceApi { - /// Base URL for the Source API endpoint - pub endpoint: String, - - /// API key for authenticating requests - api_key: String, - - /// Cache for product data to reduce API calls - product_cache: Arc>, - - /// Cache for data connection configurations - data_connection_cache: Arc>, - - /// Cache for API key credentials - access_key_cache: Arc>, - - /// Cache for user permissions - permissions_cache: Arc>>, - - // API Client - client: reqwest::Client, -} - -#[async_trait] -impl Api for SourceApi { - /// Creates and returns a backend client for a specific repository. - /// - /// This method determines the appropriate storage backend (S3 or Azure) based on - /// the repository's configuration and returns a boxed `Repository` trait object. - /// - /// # Arguments - /// - /// * `account_id` - The ID of the account owning the repository. - /// * `repository_id` - The ID of the repository. - /// - /// # Returns - /// - /// Returns a `Result` containing either a boxed `Repository` trait object - /// or an empty error `()` if the client creation fails. - async fn get_backend_client( - &self, - account_id: &str, - repository_id: &str, - ) -> Result, BackendError> { - let product = self - .get_repository_record(account_id, repository_id) - .await?; - - let Some(repository_data) = product - .metadata - .mirrors - .get(product.metadata.primary_mirror.as_str()) - else { - return Err(BackendError::SourceRepositoryMissingPrimaryMirror); - }; - - let data_connection_id = repository_data.connection_id.clone(); - let data_connection = self.get_data_connection(&data_connection_id).await?; - - match data_connection.details.provider.as_str() { - "s3" => { - let region = - if data_connection.authentication.clone().unwrap().auth_type == "s3_local" { - Region::Custom { - name: data_connection - .details - .region - .clone() - .unwrap_or("us-west-2".to_string()), - endpoint: "http://localhost:5050".to_string(), - } - } else { - Region::Custom { - name: data_connection - .details - .region - .clone() - .unwrap_or("us-east-1".to_string()), - endpoint: format!( - "https://s3.{}.amazonaws.com", - data_connection - .details - .region - .clone() - .unwrap_or("us-east-1".to_string()) - ), - } - }; - - let bucket: String = data_connection.details.bucket.clone().unwrap_or_default(); - let base_prefix: String = data_connection - .details - .base_prefix - .clone() - .unwrap_or_default(); - - let mut prefix = format!("{}{}", base_prefix, repository_data.prefix); - if prefix.ends_with('/') { - prefix = prefix[..prefix.len() - 1].to_string(); - }; - - let auth = data_connection.authentication.clone().unwrap(); - - Ok(Box::new(S3Repository { - account_id: account_id.to_string(), - repository_id: repository_id.to_string(), - region, - bucket, - base_prefix: prefix, - auth_method: auth.auth_type, - access_key_id: auth.access_key_id, - secret_access_key: auth.secret_access_key, - })) - } - "az" => { - let account_name: String = data_connection - .details - .account_name - .clone() - .unwrap_or_default(); - - let container_name: String = data_connection - .details - .container_name - .clone() - .unwrap_or_default(); - - let base_prefix: String = data_connection - .details - .base_prefix - .clone() - .unwrap_or_default(); - - Ok(Box::new(AzureRepository { - account_id: account_id.to_string(), - repository_id: repository_id.to_string(), - account_name, - container_name, - base_prefix: format!("{}{}", base_prefix, repository_data.prefix), - })) - } - err => Err(BackendError::UnexpectedDataConnectionProvider { - provider: err.to_string(), - }), - } - } - - async fn get_account( - &self, - account_id: String, - user_identity: UserIdentity, - ) -> Result { - // Create headers - let mut headers = self.build_source_headers(); - if let Some(api_key) = user_identity.api_key { - headers.insert( - reqwest::header::AUTHORIZATION, - reqwest::header::HeaderValue::from_str( - format!("{} {}", api_key.access_key_id, api_key.secret_access_key).as_str(), - ) - .unwrap(), - ); - } - - let response = self - .client - .get(format!("{}/api/v1/products/{}", self.endpoint, account_id)) - .headers(headers) - .send() - .await?; - - let product_list = - process_json_response::(response, BackendError::RepositoryNotFound) - .await?; - let mut account = Account::default(); - - for product in product_list.products { - account.repositories.push(product.product_id); - } - - Ok(account) - } -} - -impl SourceApi { - /// Creates a new Source API client with the specified configuration. - /// - /// # Arguments - /// - /// * `endpoint` - Base URL for the Source API (e.g., "https://api.source.coop") - /// * `api_key` - API key for authenticating requests - /// * `proxy_url` - Optional proxy URL for requests (e.g., "http://proxy:8080") - /// - /// # Examples - /// - /// ```rust - /// use source_data_proxy::apis::source::SourceApi; - /// - /// let api = SourceApi::new( - /// "https://api.source.coop".to_string(), - /// "your-api-key".to_string(), - /// None - /// ); - /// ``` - pub fn new(endpoint: String, api_key: String, proxy_url: Option) -> Self { - let product_cache = Arc::new( - Cache::builder() - .time_to_live(Duration::from_secs(60)) // Set TTL to 60 seconds - .build(), - ); - - let data_connection_cache = Arc::new( - Cache::builder() - .time_to_live(Duration::from_secs(60)) // Set TTL to 60 seconds - .build(), - ); - - let access_key_cache = Arc::new( - Cache::builder() - .time_to_live(Duration::from_secs(60)) // Set TTL to 60 seconds - .build(), - ); - - let permissions_cache = Arc::new( - Cache::builder() - .time_to_live(Duration::from_secs(60)) // Set TTL to 60 seconds - .build(), - ); - - let client = { - let mut client = reqwest::Client::builder() - .user_agent(concat!("source-proxy/", env!("CARGO_PKG_VERSION"))); - if let Some(proxy) = proxy_url { - client = client.proxy(reqwest::Proxy::all(proxy).unwrap()); - } - client.build().unwrap() - }; - - SourceApi { - endpoint, - api_key, - product_cache, - data_connection_cache, - access_key_cache, - permissions_cache, - client, - } - } - - /// Builds the headers for the Source API. - /// - /// # Returns - /// - /// Returns a `reqwest::header::HeaderMap` with the appropriate headers. - fn build_source_headers(&self) -> reqwest::header::HeaderMap { - const CORE_REQUEST_HEADERS: &[(&str, &str)] = &[("accept", "application/json")]; - CORE_REQUEST_HEADERS - .iter() - .map(|(name, value)| { - ( - reqwest::header::HeaderName::from_lowercase(name.as_bytes()).unwrap(), - reqwest::header::HeaderValue::from_str(value).unwrap(), - ) - }) - .collect() - } - - /// Retrieves a product record by account and product ID. - /// - /// This method fetches product information from the Source API, including - /// metadata, account details, and configuration. Results are cached for - /// improved performance. - /// - /// # Arguments - /// - /// * `account_id` - The ID of the account that owns the product - /// * `repository_id` - The ID of the product to retrieve - /// - /// # Returns - /// - /// Returns a `Result` containing either a `SourceProduct` struct with the - /// product information or a `BackendError` if the request fails. - /// - /// # Examples - /// - /// ```rust - /// use source_data_proxy::apis::source::SourceApi; - /// - /// let api = SourceApi::new( - /// "https://api.source.coop".to_string(), - /// "your-api-key".to_string(), - /// None - /// ); - /// - /// let product = api.get_repository_record("example-account", "example-product").await?; - /// println!("Product: {}", product.title); - /// ``` - pub async fn get_repository_record( - &self, - account_id: &str, - repository_id: &str, - ) -> Result { - // Try to get the cached value - let cache_key = format!("{account_id}/{repository_id}"); - - if let Some(cached_repo) = self.product_cache.get(&cache_key).await { - return Ok(cached_repo); - } - - // If not in cache, fetch it - let url = format!( - "{}/api/v1/products/{}/{}", - self.endpoint, account_id, repository_id - ); - let headers = self.build_source_headers(); - let response = self.client.get(url).headers(headers).send().await?; - let repository = - process_json_response::(response, BackendError::RepositoryNotFound) - .await?; - - // Cache the successful result - self.product_cache - .insert(cache_key, repository.clone()) - .await; - Ok(repository) - } - - async fn fetch_data_connection( - &self, - data_connection_id: &str, - ) -> Result { - let mut headers = self.build_source_headers(); - headers.insert( - reqwest::header::AUTHORIZATION, - reqwest::header::HeaderValue::from_str(&self.api_key).unwrap(), - ); - - let response = self - .client - .get(format!( - "{}/api/v1/data-connections/{}", - self.endpoint, data_connection_id - )) - .headers(headers) - .send() - .await?; - process_json_response::(response, BackendError::DataConnectionNotFound) - .await - } - - async fn get_data_connection( - &self, - data_connection_id: &str, - ) -> Result { - if let Some(cached_repo) = self.data_connection_cache.get(data_connection_id).await { - return Ok(cached_repo); - } - - // If not in cache, fetch it - match self.fetch_data_connection(data_connection_id).await { - Ok(data_connection) => { - // Cache the successful result - self.data_connection_cache - .insert(data_connection_id.to_string(), data_connection.clone()) - .await; - Ok(data_connection) - } - Err(e) => Err(e), - } - } - - pub async fn get_api_key(&self, access_key_id: &str) -> Result { - if let Some(cached_secret) = self.access_key_cache.get(access_key_id).await { - return Ok(cached_secret); - } - - // If not in cache, fetch it - if access_key_id.is_empty() { - let secret = APIKey { - access_key_id: "".to_string(), - secret_access_key: "".to_string(), - }; - self.access_key_cache - .insert(access_key_id.to_string(), secret.clone()) - .await; - Ok(secret) - } else { - let secret = self.fetch_api_key(access_key_id.to_string()).await?; - self.access_key_cache - .insert(access_key_id.to_string(), secret.clone()) - .await; - Ok(secret) - } - } - - async fn fetch_api_key(&self, access_key_id: String) -> Result { - // Create headers - let mut headers = self.build_source_headers(); - headers.insert( - reqwest::header::AUTHORIZATION, - reqwest::header::HeaderValue::from_str(&self.api_key).unwrap(), - ); - let response = self - .client - .get(format!( - "{}/api/v1/api-keys/{access_key_id}/auth", - self.endpoint - )) - .headers(headers) - .send() - .await?; - let key = process_json_response::(response, BackendError::ApiKeyNotFound).await?; - - Ok(APIKey { - access_key_id, - secret_access_key: key.secret_access_key, - }) - } - - pub async fn is_authorized( - &self, - user_identity: UserIdentity, - account_id: &str, - repository_id: &str, - permission: RepositoryPermission, - ) -> Result { - let anon: bool = user_identity.api_key.is_none(); - - // Try to get the cached value - let cache_key = if anon { - format!("{account_id}/{repository_id}") - } else { - let api_key = user_identity.clone().api_key.unwrap(); - format!("{}/{}/{}", account_id, repository_id, api_key.access_key_id) - }; - - if let Some(cache_permissions) = self.permissions_cache.get(&cache_key).await { - return Ok(cache_permissions.contains(&permission)); - } - - // If not in cache, fetch it - let permissions = self - .fetch_permission(user_identity.clone(), account_id, repository_id) - .await?; - - // Cache the successful result - self.permissions_cache - .insert(cache_key, permissions.clone()) - .await; - - Ok(permissions.contains(&permission)) - } - - pub async fn assert_authorized( - &self, - user_identity: UserIdentity, - account_id: &str, - repository_id: &str, - permission: RepositoryPermission, - ) -> Result { - let authorized = self - .is_authorized(user_identity, account_id, repository_id, permission) - .await?; - if !authorized { - return Err(BackendError::UnauthorizedError); - } - Ok(authorized) - } - - async fn fetch_permission( - &self, - user_identity: UserIdentity, - account_id: &str, - repository_id: &str, - ) -> Result, BackendError> { - // Create headers - let mut headers = self.build_source_headers(); - if let Some(api_key) = user_identity.api_key { - headers.insert( - reqwest::header::AUTHORIZATION, - reqwest::header::HeaderValue::from_str( - format!("{} {}", api_key.access_key_id, api_key.secret_access_key).as_str(), - ) - .unwrap(), - ); - } - - let response = self - .client - .get(format!( - "{}/api/v1/products/{account_id}/{repository_id}/permissions", - self.endpoint - )) - .headers(headers) - .send() - .await?; - - process_json_response::>( - response, - BackendError::RepositoryPermissionsNotFound, - ) - .await - } -} - -#[cfg(test)] -mod tests { - use super::*; - use serde_json; - - #[test] - fn test_json_parsing() { - let json_str = r#" - { - "updated_at": "2023-01-15T10:30:00.000Z", - "metadata": { - "primary_mirror": "aws-us-east-1", - "mirrors": { - "aws-us-east-1": { - "storage_type": "s3", - "is_primary": true, - "connection_id": "aws-connection-123", - "config": { "region": "us-east-1", "bucket": "example-bucket" }, - "prefix": "example-account/sample-product/" - } - }, - "tags": ["example", "test"], - "roles": { - "example-account": { - "granted_at": "2023-01-15T10:30:00.000Z", - "account_id": "example-account", - "role": "admin", - "granted_by": "example-account" - } - } - }, - "created_at": "2023-01-01T00:00:00.000Z", - "disabled": false, - "visibility": "public", - "data_mode": "open", - "account_id": "example-account", - "description": "An example product for testing purposes.", - "product_id": "sample-product", - "featured": 0, - "title": "Sample Product", - "account": { - "identity_id": "12345678-1234-1234-1234-123456789abc", - "metadata_public": { - "domains": [ - { - "created_at": "2023-01-10T12:00:00.000Z", - "domain": "example.com", - "status": "unverified" - } - ], - "location": "Example City" - }, - "updated_at": "2023-01-15T10:30:00.000Z", - "flags": ["create_repositories", "create_organizations"], - "created_at": "2023-01-01T00:00:00.000Z", - "emails": [ - { - "verified": false, - "added_at": "2023-01-01T00:00:00.000Z", - "address": "user@example.com", - "is_primary": true - } - ], - "disabled": false, - "metadata_private": {}, - "account_id": "example-account", - "name": "Example User", - "type": "individual" - } - } - "#; - - match serde_json::from_str::(json_str) { - Ok(_product) => { - println!("✅ JSON parsed successfully!"); - } - Err(e) => { - panic!("❌ JSON parsing failed: {}", e); - } - } - } -} diff --git a/src/apis/source/types.rs b/src/apis/source/types.rs deleted file mode 100644 index 3126b4b..0000000 --- a/src/apis/source/types.rs +++ /dev/null @@ -1,437 +0,0 @@ -//! Data structures and types for the Source Cooperative API. -//! -//! This module contains all the data types, enums, and structures used to interact -//! with the Source Cooperative platform, including products, accounts, permissions, -//! and storage configurations. - -use serde::{Deserialize, Serialize}; -use std::collections::HashMap; - -/// Repository access permissions for products. -/// -/// Defines the level of access a user or account has to a specific product. -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] -pub enum RepositoryPermission { - /// Read-only access to the product data - #[serde(rename = "read")] - Read, - /// Read and write access to the product data - #[serde(rename = "write")] - Write, -} - -/// Product visibility levels that control who can discover and access the product. -/// -/// This determines how the product appears in listings and search results. -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] -pub enum ProductVisibility { - /// Product is visible to everyone and appears in public listings - #[serde(rename = "public")] - Public, - /// Product is not listed publicly but can be accessed with direct link - #[serde(rename = "unlisted")] - Unlisted, - /// Product access is restricted to specific users or groups - #[serde(rename = "restricted")] - Restricted, -} - -/// Data access modes that define how users can access the product's data. -/// -/// This controls the business model and access patterns for the product. -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] -pub enum ProductDataMode { - /// Data is freely accessible to anyone - #[serde(rename = "open")] - Open, - /// Data requires a subscription to access - #[serde(rename = "subscription")] - Subscription, - /// Data is private and only accessible to authorized users - #[serde(rename = "private")] - Private, -} - -/// Supported storage backend types for product data mirrors. -/// -/// Each product can have multiple mirrors across different storage providers -/// for redundancy and performance optimization. -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] -pub enum StorageType { - /// Amazon S3 compatible storage - #[serde(rename = "s3")] - S3, - /// Microsoft Azure Blob Storage - #[serde(rename = "azure")] - Azure, - /// Google Cloud Storage - #[serde(rename = "gcs")] - Gcs, - /// MinIO object storage - #[serde(rename = "minio")] - Minio, - /// Ceph distributed storage - #[serde(rename = "ceph")] - Ceph, -} - -/// Account types in the Source Cooperative system. -/// -/// Different account types have different capabilities and metadata structures. -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] -pub enum AccountType { - /// Individual user account - #[serde(rename = "individual")] - Individual, - /// Organization or group account - #[serde(rename = "organization")] - Organization, -} - -/// Domain verification status for account domains. -/// -/// Used to track the verification state of custom domains associated with accounts. -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] -pub enum DomainStatus { - /// Domain has not been verified - #[serde(rename = "unverified")] - Unverified, - /// Domain verification is in progress - #[serde(rename = "pending")] - Pending, - /// Domain has been successfully verified - #[serde(rename = "verified")] - Verified, -} - -/// Methods available for domain verification. -/// -/// Different verification methods provide different levels of security and ease of use. -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] -pub enum VerificationMethod { - /// DNS-based verification using TXT records - #[serde(rename = "dns")] - Dns, - /// HTML-based verification using meta tags - #[serde(rename = "html")] - Html, - /// File-based verification using uploaded files - #[serde(rename = "file")] - File, -} - -/// API key credentials for authenticating with the Source API. -/// -/// Contains the access key ID and secret access key used for API authentication. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct APIKey { - /// The access key ID for API authentication - pub access_key_id: String, - /// The secret access key for API authentication - pub secret_access_key: String, -} - -/// Represents a product in the Source Cooperative system. -/// -/// A product is the main entity that contains data and metadata, similar to a repository -/// in traditional version control systems. Products can have multiple storage mirrors -/// for redundancy and performance optimization. -/// -/// # Examples -/// -/// ```rust -/// use serde_json; -/// -/// let json = r#"{ -/// "product_id": "example-product", -/// "account_id": "example-account", -/// "title": "Example Product", -/// "description": "An example product", -/// "created_at": "2023-01-01T00:00:00Z", -/// "updated_at": "2023-01-01T00:00:00Z", -/// "visibility": "public", -/// "disabled": false, -/// "data_mode": "open", -/// "featured": 0, -/// "metadata": { ... }, -/// "account": { ... } -/// }"#; -/// let product: SourceProduct = serde_json::from_str(json)?; -/// ``` -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct SourceProduct { - /// Unique identifier for the product (3-40 chars, lowercase, alphanumeric with hyphens) - pub product_id: String, - - /// ID of the account that owns this product - pub account_id: String, - - /// Human-readable title of the product - pub title: String, - - /// Detailed description of the product - pub description: String, - - /// ISO 8601 timestamp when the product was created - pub created_at: String, - - /// ISO 8601 timestamp when the product was last updated - pub updated_at: String, - - /// Visibility level of the product - pub visibility: ProductVisibility, - - /// Whether the product is disabled - pub disabled: bool, - - /// Data access mode for the product - pub data_mode: ProductDataMode, - - /// Featured status (0 = not featured, 1 = featured) - pub featured: i32, - - /// Product metadata including mirrors, tags, and roles - pub metadata: SourceProductMetadata, - - /// Optional account information - pub account: Option, -} - -/// Metadata for a product including mirrors, tags, and roles. -/// -/// Contains all the configuration and organizational information for a product -/// that doesn't fit into the main product fields. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct SourceProductMetadata { - /// Map of mirror names to mirror configurations - pub mirrors: HashMap, - - /// Name of the primary mirror (key in the mirrors map) - pub primary_mirror: String, - - /// Optional list of tags associated with the product - pub tags: Option>, -} - -/// Configuration for a storage mirror of a product. -/// -/// Each product can have multiple mirrors across different storage providers -/// for redundancy and performance optimization. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct SourceProductMirror { - /// Type of storage backend used for this mirror - pub storage_type: StorageType, - - /// ID of the data connection configuration - pub connection_id: String, - - /// Storage prefix/path for this mirror - pub prefix: String, - - /// Storage-specific configuration options - pub config: SourceProductMirrorConfig, - - /// Whether this is the primary mirror for the product - pub is_primary: bool, -} - -/// Storage-specific configuration options for a mirror. -/// -/// Different storage backends require different configuration parameters. -/// All fields are optional and only relevant for specific storage types. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct SourceProductMirrorConfig { - /// AWS region for S3/GCS storage - pub region: Option, - - /// Bucket name for S3/GCS storage - pub bucket: Option, - - /// Container name for Azure Blob Storage - pub container: Option, - - /// Custom endpoint URL for MinIO/Ceph storage - pub endpoint: Option, -} - -/// Account information associated with a product. -/// -/// Contains the account details of the product owner, including profile information, -/// contact details, and organizational metadata. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct SourceProductAccount { - /// Unique identifier for the account - pub account_id: String, - - /// Type of account (individual or organization) - #[serde(rename = "type")] - pub account_type: AccountType, - - /// Display name of the account - pub name: String, - - /// Identity provider ID (only for individual accounts) - pub identity_id: Option, - - /// Public metadata visible to other users - pub metadata_public: SourceProductAccountMetadataPublic, - - /// Email addresses associated with the account - pub emails: Option>, - - /// ISO 8601 timestamp when the account was created - pub created_at: String, - - /// ISO 8601 timestamp when the account was last updated - pub updated_at: String, - - /// Whether the account is disabled - pub disabled: bool, - - /// Account capability flags - pub flags: Vec, - - /// Private metadata not visible to other users - pub metadata_private: Option>, -} - -/// Domain verification information for an account. -/// -/// Tracks the verification status and process for custom domains associated with accounts. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct AccountDomain { - /// The domain name being verified - pub domain: String, - - /// Current verification status of the domain - pub status: DomainStatus, - - /// Method used for verification (if applicable) - pub verification_method: Option, - - /// Token used for verification (if applicable) - pub verification_token: Option, - - /// ISO 8601 timestamp when verification was completed - pub verified_at: Option, - - /// ISO 8601 timestamp when domain was added - pub created_at: String, - - /// ISO 8601 timestamp when verification expires (if applicable) - pub expires_at: Option, -} - -/// Email address information for an account. -/// -/// Tracks email addresses associated with an account, including verification status -/// and primary email designation. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct SourceAccountEmail { - /// The email address - pub address: String, - - /// Whether the email address has been verified - pub verified: bool, - - /// ISO 8601 timestamp when verification was completed - pub verified_at: Option, - - /// Whether this is the primary email address for the account - pub is_primary: bool, - - /// ISO 8601 timestamp when the email was added - pub added_at: String, -} - -/// Public metadata for an account. -/// -/// Information that is visible to other users and can be displayed in public profiles. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct SourceProductAccountMetadataPublic { - /// Optional biographical information - pub bio: Option, - - /// Verified domains associated with the account - pub domains: Option>, - - /// Geographic location of the account holder - pub location: Option, - - /// Owner account ID (for organizational accounts) - pub owner_account_id: Option, - - /// List of admin account IDs (for organizational accounts) - pub admin_account_ids: Option>, - - /// List of member account IDs (for organizational accounts) - pub member_account_ids: Option>, -} - -/// Details about a data connection configuration. -/// -/// Contains provider-specific information about how to connect to storage backends. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct DataConnectionDetails { - /// Storage provider type (e.g., "s3", "az") - pub provider: String, - /// Cloud region for the storage service - pub region: Option, - /// Base prefix for all data stored through this connection - pub base_prefix: Option, - /// S3 bucket name (for S3-compatible providers) - pub bucket: Option, - /// Azure storage account name (for Azure) - pub account_name: Option, - /// Azure container name (for Azure) - pub container_name: Option, -} - -/// Authentication configuration for a data connection. -/// -/// Defines how to authenticate with the storage backend. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct DataConnectionAuthentication { - /// Type of authentication (e.g., "s3_local", "iam_role") - #[serde(rename = "type")] - pub auth_type: String, - /// Access key ID for credential-based authentication - pub access_key_id: Option, - /// Secret access key for credential-based authentication - pub secret_access_key: Option, -} - -/// Configuration for connecting to external data storage. -/// -/// A data connection defines how products can access external storage backends -/// like S3, Azure Blob Storage, or other object storage systems. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct DataConnection { - /// Unique identifier for this data connection - pub data_connection_id: String, - /// Human-readable name for the connection - pub name: String, - /// Template for generating storage prefixes - pub prefix_template: String, - /// Whether this connection only allows read operations - pub read_only: bool, - /// List of data modes that can use this connection - pub allowed_data_modes: Vec, - /// Optional flag required on accounts to use this connection - pub required_flag: Option, - /// Provider-specific connection details - pub details: DataConnectionDetails, - /// Authentication configuration for the connection - pub authentication: Option, -} - -/// List of products with pagination support. -/// -/// Used for API responses that return multiple products. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct SourceProductList { - /// List of products in this page - pub products: Vec, - /// Token for fetching the next page of results - pub next: Option, -} diff --git a/src/backends/azure.rs b/src/backends/azure.rs deleted file mode 100644 index 58d2535..0000000 --- a/src/backends/azure.rs +++ /dev/null @@ -1,300 +0,0 @@ -use actix_web::http::header::RANGE; -use async_trait::async_trait; -use azure_core::request_options::NextMarker; -use azure_storage::StorageCredentials; -use azure_storage_blobs::container::operations::list_blobs::BlobItem; -use azure_storage_blobs::prelude::*; -use bytes::Bytes; -use core::num::NonZeroU32; -use futures::StreamExt; -use futures_core::Stream; -use reqwest; -use std::pin::Pin; -use time::format_description::well_known::{Rfc2822, Rfc3339}; - -use crate::backends::common::{ - CommonPrefix, CompleteMultipartUploadResponse, Content, CreateMultipartUploadResponse, - GetObjectResponse, HeadObjectResponse, ListBucketResult, Repository, -}; -use crate::utils::core::replace_first; -use crate::utils::errors::BackendError; - -use super::common::{MultipartPart, UploadPartResponse}; - -pub struct AzureRepository { - pub account_id: String, - pub repository_id: String, - pub account_name: String, - pub container_name: String, - pub base_prefix: String, -} - -use chrono::format::strftime::StrftimeItems; -use chrono::{DateTime, FixedOffset}; - -fn rfc2822_to_rfc7231(rfc2822_date: &str) -> Result { - // Parse the RFC2822 date string - let datetime = DateTime::parse_from_rfc2822(rfc2822_date)?; - - // Define the format string for RFC7231 - let format = StrftimeItems::new("%a, %d %b %Y %H:%M:%S GMT"); - - // Convert to UTC and format as RFC7231 - Ok(datetime - .with_timezone(&FixedOffset::east_opt(0).unwrap()) - .format_with_items(format.clone()) - .to_string()) -} - -#[async_trait] -impl Repository for AzureRepository { - async fn get_object( - &self, - key: String, - range: Option, - ) -> Result { - let credentials = StorageCredentials::anonymous(); - - let client = BlobServiceClient::new(self.account_name.to_string(), credentials) - .container_client(&self.container_name); - - let blob_client = client.blob_client(format!( - "{}/{}", - self.base_prefix.trim_end_matches('/'), - key - )); - - let blob = blob_client.get_properties().await?; - let content_type = blob.blob.properties.content_type.to_string(); - let etag = blob.blob.properties.etag.to_string(); - let last_modified = rfc2822_to_rfc7231( - blob.blob - .properties - .last_modified - .format(&Rfc2822) - .unwrap_or_else(|_| String::from("Invalid DateTime")) - .as_str(), - ) - .unwrap_or_else(|_| String::from("Invalid DateTime")); - - let client = reqwest::Client::new(); - - // Start building the request - let mut request = client.get(format!( - "https://{}.blob.core.windows.net/{}/{}/{}", - self.account_name, - self.container_name, - self.base_prefix.trim_end_matches('/'), - key - )); - - // If a range is provided, add it to the headers - if let Some(range_value) = range { - request = request.header(RANGE, range_value); - } - - // Send the request and await the response - let response = request.send().await?; - // Check if the status code is successful - if !response.status().is_success() { - return Err(BackendError::UnexpectedApiError(response.text().await?)); - } - - // Get the byte stream from the response - let content_length = response.content_length(); - let stream = response.bytes_stream(); - let boxed_stream: Pin> + Send>> = - Box::pin(stream); - - Ok(GetObjectResponse { - content_length: content_length.unwrap_or(0), - content_type, - etag, - last_modified, - body: boxed_stream, - }) - } - - async fn delete_object(&self, _key: String) -> Result<(), BackendError> { - Err(BackendError::UnsupportedOperation( - "Delete object is not supported on Azure".to_string(), - )) - } - - async fn create_multipart_upload( - &self, - _key: String, - _content_type: Option, - ) -> Result { - Err(BackendError::UnsupportedOperation( - "Create multipart upload is not supported on Azure".to_string(), - )) - } - - async fn abort_multipart_upload( - &self, - _key: String, - _upload_id: String, - ) -> Result<(), BackendError> { - Err(BackendError::UnsupportedOperation( - "Abort multipart upload is not supported on Azure".to_string(), - )) - } - - async fn complete_multipart_upload( - &self, - _key: String, - _upload_id: String, - _parts: Vec, - ) -> Result { - Err(BackendError::UnsupportedOperation( - "Complete multipart upload is not supported on Azure".to_string(), - )) - } - - async fn upload_multipart_part( - &self, - _key: String, - _upload_id: String, - _part_number: String, - _bytes: Bytes, - ) -> Result { - Err(BackendError::UnsupportedOperation( - "Upload multipart part is not supported on Azure".to_string(), - )) - } - - async fn put_object( - &self, - _key: String, - _bytes: Bytes, - _content_type: Option, - ) -> Result<(), BackendError> { - Err(BackendError::UnsupportedOperation( - "Put object is not supported on Azure".to_string(), - )) - } - - async fn head_object(&self, key: String) -> Result { - let credentials = StorageCredentials::anonymous(); - - // Create a client for anonymous access - let client = BlobServiceClient::new(self.account_name.to_string(), credentials) - .container_client(&self.container_name); - - let blob = client - .blob_client(format!( - "{}/{}", - self.base_prefix.trim_end_matches('/'), - key - )) - .get_properties() - .await?; - - Ok(HeadObjectResponse { - content_length: blob.blob.properties.content_length, - content_type: blob.blob.properties.content_type.to_string(), - etag: blob.blob.properties.etag.to_string(), - last_modified: rfc2822_to_rfc7231( - blob.blob - .properties - .last_modified - .format(&Rfc2822) - .unwrap_or_else(|_| String::from("Invalid DateTime")) - .as_str(), - ) - .unwrap_or_else(|_| String::from("Invalid DateTime")), - }) - } - - async fn list_objects_v2( - &self, - prefix: String, - continuation_token: Option, - delimiter: Option, - max_keys: NonZeroU32, - ) -> Result { - let mut result = ListBucketResult { - name: self.account_id.to_string(), - prefix: prefix.clone(), - key_count: 0, - max_keys: 0, - is_truncated: false, - contents: vec![], - common_prefixes: vec![], - next_continuation_token: None, - }; - - let credentials = StorageCredentials::anonymous(); - - // Create a client for anonymous access - let client = BlobServiceClient::new(self.account_name.to_string(), credentials) - .container_client(&self.container_name); - let search_prefix = format!("{}/{}", self.base_prefix.trim_end_matches('/'), prefix); - - let next_marker = continuation_token.map_or(NextMarker::new("".to_string()), Into::into); - - let query_delmiter = delimiter.unwrap_or_default(); - - // List blobs - let mut stream = client - .list_blobs() - .marker(next_marker) - .prefix(search_prefix) - .max_results(max_keys) - .delimiter(query_delmiter) - .into_stream(); - - if let Some(Ok(blob)) = stream.next().await { - if let Some(max_results) = blob.max_results { - result.max_keys = max_results as i64; - } - - if let Some(next_marker) = blob.next_marker { - result.is_truncated = true; - result.next_continuation_token = Some(next_marker.as_str().to_string()); - } - - for blob_item in blob.blobs.items { - match blob_item { - BlobItem::Blob(b) => { - result.contents.push(Content { - key: replace_first( - b.name, - self.base_prefix.clone().trim_end_matches('/').to_string(), - self.repository_id.to_string(), - ), - last_modified: b - .properties - .last_modified - .format(&Rfc3339) - .unwrap_or_else(|_| String::from("Invalid DateTime")), - etag: b.properties.etag.to_string(), - size: b.properties.content_length as i64, - storage_class: b.properties.blob_type.to_string(), - }); - } - BlobItem::BlobPrefix(bp) => { - result.common_prefixes.push(CommonPrefix { - prefix: replace_first( - bp.name, - self.base_prefix.clone().trim_end_matches('/').to_string(), - self.repository_id.to_string(), - ), - }); - } - } - } - } - - Ok(result) - } - async fn copy_object( - &self, - _copy_identifier_path: String, - _key: String, - _range: Option, - ) -> Result<(), BackendError> { - Ok(()) - } -} diff --git a/src/backends/common.rs b/src/backends/common.rs deleted file mode 100644 index 703db50..0000000 --- a/src/backends/common.rs +++ /dev/null @@ -1,170 +0,0 @@ -use async_trait::async_trait; -use bytes::Bytes; -use core::num::NonZeroU32; -use futures_core::Stream; -use serde::Deserialize; -use serde::Serialize; -use std::pin::Pin; - -use reqwest::Error as ReqwestError; -type BoxedReqwestStream = Pin> + Send>>; -use crate::utils::errors::BackendError; - -pub struct GetObjectResponse { - pub content_length: u64, - pub content_type: String, - pub last_modified: String, - pub etag: String, - pub body: BoxedReqwestStream, -} - -pub struct HeadObjectResponse { - pub content_length: u64, - pub content_type: String, - pub last_modified: String, - pub etag: String, -} - -#[derive(Debug, Serialize)] -pub struct CompleteMultipartUploadResponse { - #[serde(rename = "Location")] - pub location: String, - #[serde(rename = "Bucket")] - pub bucket: String, - #[serde(rename = "Key")] - pub key: String, - #[serde(rename = "ETag")] - pub etag: String, -} - -#[async_trait] -pub trait Repository { - async fn delete_object(&self, key: String) -> Result<(), BackendError>; - async fn create_multipart_upload( - &self, - key: String, - content_type: Option, - ) -> Result; - async fn abort_multipart_upload( - &self, - key: String, - upload_id: String, - ) -> Result<(), BackendError>; - async fn complete_multipart_upload( - &self, - key: String, - upload_id: String, - parts: Vec, - ) -> Result; - async fn upload_multipart_part( - &self, - key: String, - upload_id: String, - part_number: String, - bytes: Bytes, - ) -> Result; - async fn put_object( - &self, - key: String, - bytes: Bytes, - content_type: Option, - ) -> Result<(), BackendError>; - async fn get_object( - &self, - key: String, - range: Option, - ) -> Result; - async fn head_object(&self, key: String) -> Result; - async fn list_objects_v2( - &self, - prefix: String, - continuation_token: Option, - delimiter: Option, - max_keys: NonZeroU32, - ) -> Result; - async fn copy_object( - &self, - copy_identifier_path: String, - key: String, - range: Option, - ) -> Result<(), BackendError>; -} - -#[derive(Debug, Serialize)] -pub struct ListBucketResult { - #[serde(rename = "Name")] - pub name: String, - #[serde(rename = "Prefix")] - pub prefix: String, - #[serde(rename = "KeyCount")] - pub key_count: i64, - #[serde(rename = "MaxKeys")] - pub max_keys: i64, - #[serde(rename = "IsTruncated")] - pub is_truncated: bool, - #[serde(rename = "Contents")] - pub contents: Vec, - #[serde(rename = "CommonPrefixes")] - pub common_prefixes: Vec, - #[serde(rename = "NextContinuationToken")] - pub next_continuation_token: Option, -} - -#[derive(Debug, Serialize)] -pub struct Content { - #[serde(rename = "Key")] - pub key: String, - #[serde(rename = "LastModified")] - pub last_modified: String, - #[serde(rename = "ETag")] - pub etag: String, - #[serde(rename = "Size")] - pub size: i64, - #[serde(rename = "StorageClass")] - pub storage_class: String, -} - -#[derive(Debug, Serialize)] -pub struct CommonPrefix { - #[serde(rename = "Prefix")] - pub prefix: String, -} - -#[derive(Debug, Serialize)] -pub struct CreateMultipartUploadResponse { - #[serde(rename = "Bucket")] - pub bucket: String, - #[serde(rename = "Key")] - pub key: String, - #[serde(rename = "UploadId")] - pub upload_id: String, -} - -#[derive(Debug, Serialize)] -pub struct UploadPartResponse { - #[serde(rename = "ETag")] - pub etag: String, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct MultipartPart { - #[serde(rename = "PartNumber")] - pub part_number: i64, - #[serde(rename = "ETag")] - pub etag: String, - #[serde(rename = "ChecksumCRC32")] - pub checksum_crc32: Option, - #[serde(rename = "ChecksumCRC32C")] - pub checksum_crc32c: Option, - #[serde(rename = "ChecksumSHA1")] - pub checksum_sha1: Option, - #[serde(rename = "ChecksumSHA256")] - pub checksum_sha256: Option, -} - -#[derive(Debug, Deserialize)] -#[serde(rename = "CompleteMultipartUpload")] -pub struct CompleteMultipartUpload { - #[serde(rename = "Part")] - pub parts: Vec, -} diff --git a/src/backends/mod.rs b/src/backends/mod.rs deleted file mode 100644 index 0fb961c..0000000 --- a/src/backends/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod azure; -pub mod common; -pub mod s3; diff --git a/src/backends/s3.rs b/src/backends/s3.rs deleted file mode 100644 index ec758ad..0000000 --- a/src/backends/s3.rs +++ /dev/null @@ -1,382 +0,0 @@ -use super::common::{MultipartPart, UploadPartResponse}; -use crate::backends::common::{ - CommonPrefix, CompleteMultipartUploadResponse, Content, CreateMultipartUploadResponse, - GetObjectResponse, HeadObjectResponse, ListBucketResult, Repository, -}; -use crate::utils::core::replace_first; -use crate::utils::errors::BackendError; -use actix_web::http::header::RANGE; -use async_trait::async_trait; -use bytes::Bytes; -use chrono::Utc; -use core::num::NonZeroU32; -use futures_core::Stream; -use reqwest; -use rusoto_core::Region; -use rusoto_s3::{ - AbortMultipartUploadRequest, CompleteMultipartUploadRequest, CompletedMultipartUpload, - CompletedPart, CreateMultipartUploadRequest, DeleteObjectRequest, HeadObjectRequest, - ListObjectsV2Request, PutObjectRequest, S3Client, UploadPartRequest, S3, -}; -use std::pin::Pin; - -pub struct S3Repository { - pub account_id: String, - pub repository_id: String, - pub region: Region, - pub bucket: String, - pub base_prefix: String, - pub auth_method: String, - pub access_key_id: Option, - pub secret_access_key: Option, -} - -impl S3Repository { - fn create_client(&self) -> Result { - if self.auth_method == "s3_access_key" { - let credentials = rusoto_credential::StaticProvider::new_minimal( - self.access_key_id.clone().unwrap(), - self.secret_access_key.clone().unwrap(), - ); - Ok(S3Client::new_with( - rusoto_core::request::HttpClient::new().unwrap(), - credentials, - self.region.clone(), - )) - } else if self.auth_method == "s3_ecs_task_role" { - let credentials = rusoto_credential::ContainerProvider::new(); - Ok(S3Client::new_with( - rusoto_core::request::HttpClient::new().unwrap(), - credentials, - self.region.clone(), - )) - } else if self.auth_method == "s3_local" { - let credentials = rusoto_credential::ChainProvider::new(); - Ok(S3Client::new_with( - rusoto_core::request::HttpClient::new().unwrap(), - credentials, - self.region.clone(), - )) - } else { - Err(BackendError::UnsupportedAuthMethod(format!( - "Unsupported auth method: {}", - self.auth_method - ))) - } - } -} - -#[async_trait] -impl Repository for S3Repository { - async fn get_object( - &self, - key: String, - range: Option, - ) -> Result { - let head_object_response = self.head_object(key.clone()).await?; - let client = reqwest::Client::new(); - - let url = if self.auth_method == "s3_local" { - format!( - "http://localhost:5050/{}/{}/{}", - self.bucket, self.base_prefix, key - ) - } else { - format!( - "https://s3.{}.amazonaws.com/{}/{}/{}", - self.region.name(), - self.bucket, - self.base_prefix, - key - ) - }; - // Start building the request - let mut request = client.get(url); - - // If a range is provided, add it to the headers - if let Some(range_value) = range { - request = request.header(RANGE, range_value); - } - - // Send the request and await the response - let response = request.send().await?; - // Get the byte stream from the response - let content_length = response.content_length(); - let stream = response.bytes_stream(); - let boxed_stream: Pin> + Send>> = - Box::pin(stream); - - Ok(GetObjectResponse { - content_length: content_length.unwrap_or(0), - content_type: head_object_response.content_type, - etag: head_object_response.etag, - last_modified: head_object_response.last_modified, - body: boxed_stream, - }) - } - - async fn put_object( - &self, - key: String, - bytes: Bytes, - content_type: Option, - ) -> Result<(), BackendError> { - let client = self.create_client()?; - - let request = PutObjectRequest { - bucket: self.bucket.clone(), - key: format!("{}/{}", self.base_prefix, key), - body: Some(bytes.to_vec().into()), - content_type, - ..Default::default() - }; - - client.put_object(request).await?; - Ok(()) - } - - async fn create_multipart_upload( - &self, - key: String, - content_type: Option, - ) -> Result { - let client = self.create_client()?; - - let request = CreateMultipartUploadRequest { - bucket: self.bucket.clone(), - key: format!("{}/{}", self.base_prefix, key), - content_type, - ..Default::default() - }; - - let result = client.create_multipart_upload(request).await?; - Ok(CreateMultipartUploadResponse { - bucket: self.account_id.clone(), - key: key.clone(), - upload_id: result.upload_id.unwrap(), - }) - } - - async fn abort_multipart_upload( - &self, - key: String, - upload_id: String, - ) -> Result<(), BackendError> { - let client = self.create_client()?; - - let request = AbortMultipartUploadRequest { - bucket: self.bucket.clone(), - key: format!("{}/{}", self.base_prefix, key), - upload_id, - ..Default::default() - }; - - client.abort_multipart_upload(request).await?; - Ok(()) - } - - async fn complete_multipart_upload( - &self, - key: String, - upload_id: String, - parts: Vec, - ) -> Result { - let client = self.create_client()?; - - let request = CompleteMultipartUploadRequest { - bucket: self.bucket.clone(), - key: format!("{}/{}", self.base_prefix, key), - upload_id, - multipart_upload: Some(CompletedMultipartUpload { - parts: Some( - parts - .iter() - .map(|part| CompletedPart { - e_tag: Some(part.etag.clone()), - part_number: Some(part.part_number), - }) - .collect(), - ), - }), - ..Default::default() - }; - - let result = client.complete_multipart_upload(request).await?; - Ok(CompleteMultipartUploadResponse { - location: "".to_string(), - bucket: self.account_id.clone(), - key: key.clone(), - etag: result.e_tag.unwrap(), - }) - } - - async fn upload_multipart_part( - &self, - key: String, - upload_id: String, - part_number: String, - bytes: Bytes, - ) -> Result { - let client = self.create_client()?; - - let request = UploadPartRequest { - bucket: self.bucket.clone(), - key: format!("{}/{}", self.base_prefix, key), - upload_id, - part_number: part_number.parse().unwrap(), - body: Some(bytes.to_vec().into()), - ..Default::default() - }; - - let result = client.upload_part(request).await?; - Ok(UploadPartResponse { - etag: result.e_tag.unwrap(), - }) - } - - async fn delete_object(&self, key: String) -> Result<(), BackendError> { - let client = self.create_client()?; - - let request = DeleteObjectRequest { - bucket: self.bucket.clone(), - key: format!("{}/{}", self.base_prefix, key), - ..Default::default() - }; - - client.delete_object(request).await?; - Ok(()) - } - - async fn head_object(&self, key: String) -> Result { - let client = self.create_client()?; - - let request = HeadObjectRequest { - bucket: self.bucket.clone(), - key: format!("{}/{}", self.base_prefix, key), - ..Default::default() - }; - - let result = client.head_object(request).await?; - - Ok(HeadObjectResponse { - content_length: result.content_length.unwrap_or(0) as u64, - content_type: result.content_type.unwrap_or_else(|| "".to_string()), - etag: result.e_tag.unwrap_or_else(|| "".to_string()), - last_modified: result - .last_modified - .unwrap_or_else(|| Utc::now().to_rfc2822()), - }) - } - - async fn list_objects_v2( - &self, - prefix: String, - continuation_token: Option, - delimiter: Option, - max_keys: NonZeroU32, - ) -> Result { - let client = self.create_client()?; - - let mut request = ListObjectsV2Request { - bucket: self.bucket.clone(), - prefix: Some(format!("{}/{}", self.base_prefix, prefix)), - delimiter, - max_keys: Some(max_keys.get() as i64), - ..Default::default() - }; - - if let Some(token) = continuation_token { - request.continuation_token = Some(token); - } - - let output = client.list_objects_v2(request).await?; - let result = ListBucketResult { - name: self.account_id.to_string(), - prefix: format!("{}/{}", self.repository_id, prefix), - key_count: output.key_count.unwrap_or(0), - max_keys: output.max_keys.unwrap_or(0), - is_truncated: output.is_truncated.unwrap_or(false), - next_continuation_token: output.next_continuation_token, - contents: output - .contents - .unwrap_or_default() - .iter() - .map(|item| Content { - key: replace_first( - item.key.clone().unwrap_or_default(), - self.base_prefix.clone(), - self.repository_id.to_string(), - ), - last_modified: item - .last_modified - .clone() - .unwrap_or_else(|| Utc::now().to_rfc2822()), - etag: item.e_tag.clone().unwrap_or_default(), - size: item.size.unwrap_or(0), - storage_class: item.storage_class.clone().unwrap_or_default(), - }) - .collect(), - common_prefixes: output - .common_prefixes - .unwrap_or_default() - .iter() - .map(|item| CommonPrefix { - prefix: replace_first( - item.prefix.clone().unwrap_or_default(), - self.base_prefix.clone(), - self.repository_id.to_string(), - ), - }) - .collect(), - }; - - Ok(result) - } - - async fn copy_object( - &self, - copy_identifier_path: String, - key: String, - range: Option, - ) -> Result<(), BackendError> { - let client = self.create_client()?; - - let request = HeadObjectRequest { - bucket: self.bucket.clone(), - key: copy_identifier_path.to_string(), - ..Default::default() - }; - - let result = client.head_object(request).await?; - let url_client = reqwest::Client::new(); - - let url = if self.auth_method == "s3_local" { - format!( - "http://localhost:5050/{}/{}", - self.bucket, copy_identifier_path - ) - } else { - format!( - "https://s3.{}.amazonaws.com/{}/{}", - self.region.name(), - self.bucket, - copy_identifier_path - ) - }; - - let mut request = url_client.get(url); - - if let Some(range_value) = range { - request = request.header(RANGE, range_value); - } - - let response = request.send().await?; - let content_bytes = response - .bytes() - .await - .unwrap_or_else(|_| bytes::Bytes::from(vec![])); - self.put_object(key.clone(), content_bytes, result.content_type) - .await?; - Ok(()) - } -} diff --git a/src/cache.rs b/src/cache.rs new file mode 100644 index 0000000..91d205d --- /dev/null +++ b/src/cache.rs @@ -0,0 +1,168 @@ +//! Cloudflare Cache API wrapper for Source Cooperative API responses. +//! +//! Each public function caches one API call type with its own TTL. +//! Adjust the `*_CACHE_SECS` constants to tune per-datatype expiry. + +use crate::registry::{DataConnection, SourceProduct, SourceProductList}; +use multistore::error::ProxyError; + +// ── Per-datatype TTLs ────────────────────────────────────────────── +// Tune these to control how long each API response is cached at the edge. + +/// Product metadata (`/api/v1/products/{account}/{product}`). +const PRODUCT_CACHE_SECS: u32 = 300; // 5 minutes + +/// Data connections list (`/api/v1/data-connections`). +const DATA_CONNECTIONS_CACHE_SECS: u32 = 1800; // 30 minutes + +/// Product list for an account (`/api/v1/products/{account}`). +const PRODUCT_LIST_CACHE_SECS: u32 = 60; // 1 minute + +// ── Public cache functions ───────────────────────────────────────── + +/// Fetch a single product's metadata, cached for `PRODUCT_CACHE_SECS`. +pub async fn get_or_fetch_product( + api_base_url: &str, + account: &str, + product: &str, + api_secret: Option<&str>, + request_id: &str, +) -> Result { + let api_url = format!("{}/api/v1/products/{}/{}", api_base_url, account, product); + cached_fetch( + &api_url, + &api_url, + PRODUCT_CACHE_SECS, + api_secret, + request_id, + ) + .await +} + +/// Fetch all data connections, cached for `DATA_CONNECTIONS_CACHE_SECS`. +pub async fn get_or_fetch_data_connections( + api_base_url: &str, + api_secret: Option<&str>, + request_id: &str, +) -> Result, ProxyError> { + let api_url = format!("{}/api/v1/data-connections", api_base_url); + cached_fetch( + &api_url, + &api_url, + DATA_CONNECTIONS_CACHE_SECS, + api_secret, + request_id, + ) + .await +} + +/// Fetch an account's product list, cached for `PRODUCT_LIST_CACHE_SECS`. +pub async fn get_or_fetch_product_list( + api_base_url: &str, + account: &str, + api_secret: Option<&str>, + request_id: &str, +) -> Result { + let api_url = format!("{}/api/v1/products/{}", api_base_url, account); + cached_fetch( + &api_url, + &api_url, + PRODUCT_LIST_CACHE_SECS, + api_secret, + request_id, + ) + .await +} + +// ── Internal helper ──────────────────────────────────────────────── + +/// Generic cache-or-fetch: check the Cache API, return cached JSON on hit, +/// otherwise fetch from `api_url`, store in cache with the given TTL, and +/// return the deserialized result. +async fn cached_fetch( + cache_key: &str, + api_url: &str, + ttl_secs: u32, + api_secret: Option<&str>, + request_id: &str, +) -> Result { + let span = tracing::info_span!( + "cached_fetch", + cache_key = %cache_key, + cache_hit = tracing::field::Empty, + api_status = tracing::field::Empty, + ); + let _guard = span.enter(); + + let cache = worker::Cache::default(); + + // ── Cache hit ────────────────────────────────────────────── + if let Some(mut cached_resp) = cache + .get(cache_key, false) + .await + .map_err(|e| ProxyError::Internal(format!("cache get failed: {}", e)))? + { + span.record("cache_hit", true); + let text = cached_resp + .text() + .await + .map_err(|e| ProxyError::Internal(format!("cache body read failed: {}", e)))?; + return serde_json::from_str(&text) + .map_err(|e| ProxyError::Internal(format!("cache JSON parse failed: {}", e))); + } + + // ── Cache miss — fetch from API ──────────────────────────── + span.record("cache_hit", false); + let init = web_sys::RequestInit::new(); + init.set_method("GET"); + let req_headers = web_sys::Headers::new() + .map_err(|e| ProxyError::Internal(format!("headers build failed: {:?}", e)))?; + if let Some(secret) = api_secret { + req_headers + .set("Authorization", secret) + .map_err(|e| ProxyError::Internal(format!("header set failed: {:?}", e)))?; + } + if !request_id.is_empty() { + let _ = req_headers.set("x-request-id", request_id); + } + init.set_headers(&req_headers); + let req = web_sys::Request::new_with_str_and_init(api_url, &init) + .map_err(|e| ProxyError::Internal(format!("request build failed: {:?}", e)))?; + let worker_req: worker::Request = req.into(); + let mut resp = worker::Fetch::Request(worker_req) + .send() + .await + .map_err(|e| ProxyError::Internal(format!("api fetch failed: {}", e)))?; + + let status = resp.status_code(); + span.record("api_status", status); + if status == 404 { + return Err(ProxyError::BucketNotFound("not found".into())); + } + if status != 200 { + return Err(ProxyError::Internal(format!( + "API returned {} for {}", + status, api_url + ))); + } + + let text = resp + .text() + .await + .map_err(|e| ProxyError::Internal(format!("body read failed: {}", e)))?; + + // ── Store in cache ───────────────────────────────────────── + let headers = worker::Headers::new(); + let _ = headers.set("content-type", "application/json"); + let _ = headers.set("cache-control", &format!("max-age={}", ttl_secs)); + if let Ok(cache_resp) = worker::Response::ok(&text) { + let cache_resp = cache_resp.with_headers(headers); + if let Err(e) = cache.put(cache_key, cache_resp).await { + tracing::warn!("cache put failed: {}", e); + } + } + + // ── Deserialize and return ───────────────────────────────── + serde_json::from_str(&text) + .map_err(|e| ProxyError::Internal(format!("JSON parse failed: {} for {}", e, api_url))) +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..d2b9183 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,273 @@ +mod cache; +pub mod pagination; +mod registry; +pub mod routing; + +use multistore::api::list::parse_list_query_params; +use multistore::api::response::{ErrorResponse, ListBucketResult, ListCommonPrefix}; +use multistore::proxy::{GatewayResponse, ProxyGateway}; +use multistore::route_handler::RequestInfo; +use multistore_cf_workers::{ + collect_js_body, convert_ws_headers, forward_response_to_ws, proxy_result_to_ws_response, + ws_error_response, ws_xml_response, JsBody, NoopCredentialRegistry, WorkerBackend, + WorkerSubscriber, +}; +use multistore_path_mapping::{MappedRegistry, PathMapping}; +use pagination::paginate_prefixes; +use registry::SourceCoopRegistry; +use routing::{classify_request, RequestClass}; +use worker::{event, Context, Env, Result}; + +const VERSION: &str = env!("CARGO_PKG_VERSION"); + +/// Source Cooperative path mapping: `/{account}/{product}/{key}` +/// → internal bucket `account--product`, display name shows just `account`. +fn source_coop_mapping() -> PathMapping { + PathMapping { + bucket_segments: 2, + bucket_separator: "--".to_string(), + display_bucket_segments: 1, + } +} + +#[event(fetch)] +async fn fetch(req: web_sys::Request, env: Env, _ctx: Context) -> Result { + console_error_panic_hook::set_once(); + + // ── Tracing ──────────────────────────────────────────────────── + let log_level = env + .var("LOG_LEVEL") + .map(|v| v.to_string()) + .unwrap_or_else(|_| "WARN".to_string()); + let max_level = match log_level.to_uppercase().as_str() { + "TRACE" => tracing::Level::TRACE, + "DEBUG" => tracing::Level::DEBUG, + "INFO" => tracing::Level::INFO, + "ERROR" => tracing::Level::ERROR, + _ => tracing::Level::WARN, + }; + tracing::subscriber::set_global_default(WorkerSubscriber::new().with_max_level(max_level)).ok(); + + // ── Configuration ────────────────────────────────────────────── + let api_base_url = env + .var("SOURCE_API_URL") + .map(|v| v.to_string()) + .unwrap_or_else(|_| "https://source.coop".to_string()); + let api_secret = env.secret("SOURCE_API_SECRET").map(|v| v.to_string()).ok(); + + // ── Parse request ────────────────────────────────────────────── + let js_body = JsBody(req.body()); + let method: http::Method = req.method().parse().unwrap_or(http::Method::GET); + let url_str = req.url(); + let uri: http::Uri = url_str + .parse() + .unwrap_or_else(|_| http::Uri::from_static("/")); + let path = percent_encoding::percent_decode_str(uri.path()) + .decode_utf8_lossy() + .to_string(); + let query = uri.query().map(|q| q.to_string()); + let mut headers = convert_ws_headers(&req.headers()); + + // Strip AWS auth headers — this proxy is anonymous-only. + headers.remove(http::header::AUTHORIZATION); + headers.remove("x-amz-security-token"); + headers.remove("x-amz-content-sha256"); + + // ── Request ID ───────────────────────────────────────────────── + let request_id = headers + .get("cf-ray") + .and_then(|v| v.to_str().ok().map(|s| s.to_string())) + .unwrap_or_default(); + + let mapping = source_coop_mapping(); + let registry = SourceCoopRegistry::new(api_base_url, api_secret, request_id.clone()); + let mapped_registry = MappedRegistry::new(registry.clone(), mapping.clone()); + + let gateway = ProxyGateway::new(WorkerBackend, mapped_registry, NoopCredentialRegistry, None) + .with_debug_errors(max_level >= tracing::Level::DEBUG); + + // ── OPTIONS preflight ────────────────────────────────────────── + if method == http::Method::OPTIONS { + return Ok(add_cors(ws_error_response(204, ""))); + } + + // ── Reject write methods ─────────────────────────────────────── + if matches!( + method, + http::Method::PUT | http::Method::POST | http::Method::DELETE | http::Method::PATCH + ) { + return Ok(add_cors(s3_error_response( + 405, + "MethodNotAllowed", + "Method Not Allowed", + &request_id, + ))); + } + + let request_span = tracing::info_span!( + "request", + request_id = %request_id, + method = %method, + path = %path, + status = tracing::field::Empty, + request_class = tracing::field::Empty, + ); + let _request_guard = request_span.enter(); + + let response = match classify_request(&mapping, &path, query.as_deref()) { + RequestClass::Index => { + request_span.record("request_class", "index"); + ws_error_response(200, &format!("Source Cooperative Data Proxy v{}", VERSION)) + } + + RequestClass::BadRequest(msg) => { + request_span.record("request_class", "bad_request"); + s3_error_response(400, "InvalidRequest", &msg, &request_id) + } + + RequestClass::AccountList { account, query: q } => { + request_span.record("request_class", "account_list"); + handle_account_list(®istry, &account, q.as_deref()).await + } + + RequestClass::ProxyRequest { + rewritten_path, + query: q, + } => { + request_span.record("request_class", "proxy"); + let req_info = RequestInfo::new(&method, &rewritten_path, q.as_deref(), &headers, None); + dispatch_to_gateway(&gateway, &req_info, js_body, &rewritten_path).await + } + }; + + request_span.record("status", response.status()); + + let response = add_cors(response); + if !request_id.is_empty() { + let _ = response.headers().set("x-request-id", &request_id); + } + Ok(response) +} + +// ── Request classification (see routing.rs) ─────────────────────── + +// ── Account listing ──────────────────────────────────────────────── + +/// Handle `GET /{account}?list-type=2` — list products via the Source Coop API. +async fn handle_account_list( + registry: &SourceCoopRegistry, + account: &str, + query: Option<&str>, +) -> web_sys::Response { + let params = parse_list_query_params(query); + + let all_prefixes: Vec = match registry.list_products(account).await { + Ok(products) => products.into_iter().map(|p| format!("{p}/")).collect(), + Err(e) => { + tracing::error!("AccountList({}) error: {:?}", account, e); + vec![] + } + }; + + let paginated = paginate_prefixes(all_prefixes, ¶ms); + + let common_prefixes: Vec = paginated + .prefixes + .into_iter() + .map(|prefix| ListCommonPrefix { prefix }) + .collect(); + + let result = ListBucketResult { + xmlns: "http://s3.amazonaws.com/doc/2006-03-01/", + name: account.to_string(), + prefix: String::new(), + delimiter: "/".to_string(), + encoding_type: params.encoding_type, + max_keys: params.max_keys, + is_truncated: paginated.is_truncated, + key_count: common_prefixes.len(), + start_after: params.start_after, + continuation_token: params.continuation_token, + next_continuation_token: paginated.next_continuation_token, + contents: vec![], + common_prefixes, + }; + ws_xml_response(200, &result.to_xml()) +} + +// ── Gateway dispatch ─────────────────────────────────────────────── + +/// Dispatch a request through the ProxyGateway and convert the result to a web_sys::Response. +async fn dispatch_to_gateway( + gateway: &ProxyGateway< + WorkerBackend, + MappedRegistry, + NoopCredentialRegistry, + >, + req_info: &RequestInfo<'_>, + js_body: JsBody, + rewritten_path: &str, +) -> web_sys::Response { + let result = gateway + .handle_request(req_info, js_body, collect_js_body) + .await; + + match &result { + GatewayResponse::Response(ref r) if r.status >= 400 => { + let body_str = match &r.body { + multistore::route_handler::ProxyResponseBody::Bytes(b) => { + std::str::from_utf8(b).unwrap_or("").to_string() + } + multistore::route_handler::ProxyResponseBody::Empty => "".to_string(), + }; + if r.status >= 500 { + tracing::error!("{} returned {}: {}", rewritten_path, r.status, body_str); + } else { + tracing::warn!("{} returned {}: {}", rewritten_path, r.status, body_str); + } + } + GatewayResponse::Forward(ref r) if r.status >= 400 => { + if r.status >= 500 { + tracing::error!("{} forwarded {}", rewritten_path, r.status); + } else { + tracing::warn!("{} forwarded {}", rewritten_path, r.status); + } + } + _ => {} + } + + match result { + GatewayResponse::Response(result) => proxy_result_to_ws_response(result), + GatewayResponse::Forward(resp) => forward_response_to_ws(resp), + } +} + +// ── S3-format errors ──────────────────────────────────────────────── + +/// Build an S3-compatible XML error response. +fn s3_error_response( + status: u16, + code: &str, + message: &str, + request_id: &str, +) -> web_sys::Response { + let err = ErrorResponse { + code: code.to_string(), + message: message.to_string(), + resource: String::new(), + request_id: request_id.to_string(), + }; + ws_xml_response(status, &err.to_xml()) +} + +// ── CORS ─────────────────────────────────────────────────────────── + +/// Add CORS headers to a response. +fn add_cors(resp: web_sys::Response) -> web_sys::Response { + let h = resp.headers(); + let _ = h.set("access-control-allow-origin", "*"); + let _ = h.set("access-control-allow-methods", "GET, HEAD, OPTIONS"); + let _ = h.set("access-control-allow-headers", "*"); + let _ = h.set("access-control-expose-headers", "*"); + resp +} diff --git a/src/main.rs b/src/main.rs deleted file mode 100644 index 3f87297..0000000 --- a/src/main.rs +++ /dev/null @@ -1,525 +0,0 @@ -mod apis; -mod backends; -mod utils; -use crate::utils::core::{split_at_first_slash, RangeRequest, StreamingResponse}; -use actix_cors::Cors; -use actix_web::body::{BodySize, BoxBody, MessageBody}; -use actix_web::error::ErrorInternalServerError; -use actix_web::{ - delete, get, head, - http::header::{CONTENT_TYPE, RANGE}, - middleware, post, put, web, App, HttpRequest, HttpResponse, HttpServer, Responder, -}; - -use apis::source::{RepositoryPermission, SourceApi}; -use apis::Api; -use backends::common::{CommonPrefix, CompleteMultipartUpload, ListBucketResult}; -use bytes::Bytes; -use core::num::NonZeroU32; -use env_logger::Env; -use futures_util::StreamExt; -use quick_xml::se::to_string_with_root; -use serde::Deserialize; -use serde_xml_rs::from_str; -use std::env; -use std::fmt::Debug; -use std::pin::Pin; -use std::str::from_utf8; -use std::task::{Context, Poll}; -use utils::auth::{LoadIdentity, UserIdentity}; -use utils::errors::BackendError; -const VERSION: &str = env!("CARGO_PKG_VERSION"); - -struct FakeBody { - size: usize, -} - -impl MessageBody for FakeBody { - type Error = actix_web::Error; - - fn size(&self) -> BodySize { - BodySize::Sized(self.size as u64) - } - - fn poll_next( - self: Pin<&mut Self>, - _: &mut Context<'_>, - ) -> Poll>> { - Poll::Ready(None) - } -} - -#[get("/{account_id}/{repository_id}/{key:.*}")] -async fn get_object( - api_client: web::Data, - req: HttpRequest, - path: web::Path<(String, String, String)>, - user_identity: web::ReqData, -) -> Result { - let (account_id, repository_id, key) = path.into_inner(); - let range_info: Option = req - .headers() - .get(RANGE) - .and_then(|v| String::from_utf8(v.as_ref().to_vec()).ok()) - .and_then(|s| s.parse().ok()); - - let client = api_client - .get_backend_client(&account_id, &repository_id) - .await?; - - api_client - .assert_authorized( - user_identity.into_inner(), - &account_id, - &repository_id, - RepositoryPermission::Read, - ) - .await?; - - // Found the repository, now try to get the object - let res = client - .get_object(key.clone(), range_info.map(Into::into)) - .await?; - - let mut total_content_length = String::from("*"); - if range_info.is_some() { - total_content_length = client - .head_object(key.clone()) - .await? - .content_length - .to_string(); - } - - let stream = res - .body - .map(|result| result.map_err(|e| ErrorInternalServerError(e.to_string()))); - - let streaming_response = StreamingResponse::new(stream, res.content_length); - let mut response = if range_info.is_some() { - HttpResponse::PartialContent() - } else { - HttpResponse::Ok() - }; - - let mut response = response - .insert_header(("Accept-Ranges", "bytes")) - .insert_header(("Access-Control-Expose-Headers", "Accept-Ranges")) - .insert_header(("Content-Type", res.content_type)) - .insert_header(("Last-Modified", res.last_modified)) - .insert_header(("Content-Length", res.content_length.to_string())) - .insert_header(("ETag", res.etag)); - - if let Some(ref range) = range_info { - response = response - .insert_header(( - "Content-Range", - format!( - "bytes {}-{}/{}", - range.start, - range.start + res.content_length - 1, - total_content_length - ), - )) - .insert_header(( - "Access-Control-Expose-Headers", - "Accept-Ranges, Content-Range", - )); - } - - Ok(response.body(streaming_response)) -} - -#[derive(Debug, Deserialize)] -struct DeleteParams { - #[serde(rename = "uploadId")] - upload_id: Option, -} - -#[delete("/{account_id}/{repository_id}/{key:.*}")] -async fn delete_object( - api_client: web::Data, - params: web::Query, - path: web::Path<(String, String, String)>, - user_identity: web::ReqData, -) -> Result { - let (account_id, repository_id, key) = path.into_inner(); - - let client = api_client - .get_backend_client(&account_id, &repository_id) - .await?; - - api_client - .assert_authorized( - user_identity.into_inner(), - &account_id, - &repository_id, - RepositoryPermission::Write, - ) - .await?; - - if params.upload_id.is_none() { - // Found the repository, now try to delete the object - client.delete_object(key.clone()).await?; - Ok(HttpResponse::NoContent().finish()) - } else { - client - .abort_multipart_upload(key.clone(), params.upload_id.clone().unwrap()) - .await?; - Ok(HttpResponse::NoContent().finish()) - } -} - -#[derive(Debug, Deserialize)] -struct PutParams { - #[serde(rename = "partNumber")] - part_number: Option, - #[serde(rename = "uploadId")] - upload_id: Option, -} - -#[put("/{account_id}/{repository_id}/{key:.*}")] -async fn put_object( - api_client: web::Data, - req: HttpRequest, - bytes: Bytes, - params: web::Query, - path: web::Path<(String, String, String)>, - user_identity: web::ReqData, -) -> Result { - let (account_id, repository_id, key) = path.into_inner(); - let headers = req.headers(); - - let client = api_client - .get_backend_client(&account_id, &repository_id) - .await?; - - api_client - .assert_authorized( - user_identity.into_inner(), - &account_id, - &repository_id, - RepositoryPermission::Write, - ) - .await?; - - if params.part_number.is_none() && params.upload_id.is_none() { - // Check if this is a server-side copy operation - if let Some(header_copy_identifier) = req.headers().get("x-amz-copy-source") { - let copy_identifier_path = header_copy_identifier.to_str().unwrap_or(""); - client - .copy_object((©_identifier_path).to_string(), key.clone(), None) - .await?; - Ok(HttpResponse::NoContent().finish()) - } else { - // Found the repository, now try to upload the object - client - .put_object( - key.clone(), - bytes, - headers - .get(CONTENT_TYPE) - .and_then(|h| h.to_str().ok()) - .map(|s| s.to_string()), - ) - .await?; - Ok(HttpResponse::NoContent().finish()) - } - } else if params.part_number.is_some() && params.upload_id.is_some() { - let res = client - .upload_multipart_part( - key.clone(), - params.upload_id.clone().unwrap(), - params.part_number.clone().unwrap(), - bytes, - ) - .await?; - Ok(HttpResponse::Ok() - .insert_header(("ETag", res.etag)) - .finish()) - } else { - Err(BackendError::InvalidRequest( - "Must provide both part number and upload id or neither.".to_string(), - )) - } -} - -#[derive(Debug, Deserialize)] -struct PostParams { - uploads: Option, - #[serde(rename = "uploadId")] - upload_id: Option, -} - -#[post("/{account_id}/{repository_id}/{key:.*}")] -async fn post_handler( - api_client: web::Data, - req: HttpRequest, - params: web::Query, - mut payload: web::Payload, - path: web::Path<(String, String, String)>, - user_identity: web::ReqData, -) -> Result { - let (account_id, repository_id, key) = path.into_inner(); - let headers = req.headers(); - - let client = api_client - .get_backend_client(&account_id, &repository_id) - .await?; - - api_client - .assert_authorized( - user_identity.into_inner(), - &account_id, - &repository_id, - RepositoryPermission::Write, - ) - .await?; - - if params.uploads.is_some() { - let res = client - .create_multipart_upload( - key, - headers - .get(CONTENT_TYPE) - .and_then(|h| h.to_str().ok()) - .map(|s| s.to_string()), - ) - .await?; - let serialized = to_string_with_root("InitiateMultipartUploadResult", &res)?; - Ok(HttpResponse::Ok() - .content_type("application/xml") - .body(serialized)) - } else if params.upload_id.is_some() { - let mut body = String::new(); - while let Some(chunk) = payload.next().await { - match chunk { - Ok(chunk) => match from_utf8(&chunk) { - Ok(s) => body.push_str(s), - Err(_) => { - return Err(BackendError::InvalidRequest("Invalid UTF-8".to_string())) - } - }, - Err(err) => return Err(BackendError::UnexpectedApiError(err.to_string())), - } - } - - let upload = from_str::(&body)?; - let res = client - .complete_multipart_upload(key, params.upload_id.clone().unwrap(), upload.parts) - .await?; - let serialized = to_string_with_root("CompleteMultipartUploadResult", &res)?; - Ok(HttpResponse::Ok() - .content_type("application/xml") - .body(serialized)) - } else { - Err(BackendError::InvalidRequest( - "Must provide either uploads or uploadId".to_string(), - )) - } -} - -#[head("/{account_id}/{repository_id}/{key:.*}")] -async fn head_object( - api_client: web::Data, - req: HttpRequest, - path: web::Path<(String, String, String)>, - user_identity: web::ReqData, -) -> Result { - let (account_id, repository_id, key) = path.into_inner(); - - let client = api_client - .get_backend_client(&account_id, &repository_id) - .await?; - - api_client - .assert_authorized( - user_identity.into_inner(), - &account_id, - &repository_id, - RepositoryPermission::Read, - ) - .await?; - - let res = client.head_object(key.clone()).await?; - let total_size = res.content_length; - let range_info: Option = req - .headers() - .get(RANGE) - .and_then(|v| v.to_str().ok()) - .and_then(|s| s.parse().ok()); - - let response = if let Some(ref range) = range_info { - let end = range.end.unwrap(); - let content_length = end - range.start + 1; - HttpResponse::PartialContent() - .insert_header(("Accept-Ranges", "bytes")) - .insert_header(( - "Content-Range", - format!("bytes {}-{}/{}", range.start, end, total_size), - )) - .insert_header(( - "Access-Control-Expose-Headers", - "Accept-Ranges, Content-Range", - )) - .insert_header(("Content-Type", res.content_type)) - .insert_header(("Last-Modified", res.last_modified)) - .insert_header(("ETag", res.etag)) - .body(BoxBody::new(FakeBody { - size: content_length as usize, - })) - } else { - HttpResponse::Ok() - .insert_header(("Accept-Ranges", "bytes")) - .insert_header(("Access-Control-Expose-Headers", "Accept-Ranges")) - .insert_header(("Content-Type", res.content_type)) - .insert_header(("Last-Modified", res.last_modified)) - .insert_header(("ETag", res.etag)) - .body(BoxBody::new(FakeBody { - size: total_size as usize, - })) - }; - - Ok(response) -} - -#[derive(Deserialize)] -struct ListObjectsV2Query { - #[serde(rename = "prefix")] - prefix: Option, - #[serde(rename = "list-type")] - _list_type: u8, - #[serde(rename = "max-keys")] - max_keys: Option, - #[serde(rename = "delimiter")] - delimiter: Option, - #[serde(rename = "continuation-token")] - continuation_token: Option, -} - -#[get("/{account_id}")] -async fn list_objects( - api_client: web::Data, - info: web::Query, - path: web::Path, - user_identity: web::ReqData, -) -> Result { - let account_id = path.into_inner(); - - if info.prefix.clone().is_some_and(|s| s.is_empty()) || info.prefix.is_none() { - let account = api_client - .get_account(account_id.clone(), (*user_identity).clone()) - .await?; - - let repositories = account.repositories; - let mut common_prefixes = Vec::new(); - for repository_id in repositories.iter() { - common_prefixes.push(CommonPrefix { - prefix: format!("{}/", repository_id.clone()), - }); - } - let list_response = ListBucketResult { - name: account_id.clone(), - prefix: "/".to_string(), - key_count: 0, - max_keys: 0, - is_truncated: false, - contents: vec![], - common_prefixes, - next_continuation_token: None, - }; - - let serialized = to_string_with_root("ListBucketResult", &list_response)?; - return Ok(HttpResponse::Ok() - .content_type("application/xml") - .body(serialized)); - } - - let path_prefix = info.prefix.clone().unwrap_or("".to_string()); - - let (repository_id, prefix) = split_at_first_slash(&path_prefix); - - let mut max_keys = NonZeroU32::new(1000).unwrap(); - if let Some(mk) = info.max_keys { - max_keys = mk; - } - - let client = api_client - .get_backend_client(&account_id, repository_id) - .await?; - - api_client - .assert_authorized( - user_identity.into_inner(), - &account_id, - repository_id, - RepositoryPermission::Read, - ) - .await?; - - // We're listing within a repository, so we need to query the object store backend - let res = client - .list_objects_v2( - prefix.to_string(), - info.continuation_token.clone(), - info.delimiter.clone(), - max_keys, - ) - .await?; - - let serialized = to_string_with_root("ListBucketResult", &res)?; - - Ok(HttpResponse::Ok() - .content_type("application/xml") - .body(serialized)) -} - -#[get("/")] -async fn index() -> impl Responder { - HttpResponse::Ok().body(format!("Source Cooperative Data Proxy v{VERSION}")) -} - -// Main function to set up and run the HTTP server -#[actix_web::main] -async fn main() -> std::io::Result<()> { - let source_api_url = env::var("SOURCE_API_URL").expect("SOURCE_API_URL must be set"); - let source_api_key = env::var("SOURCE_API_KEY").expect("SOURCE_API_KEY must be set"); - let source_api_proxy_url = env::var("SOURCE_API_PROXY_URL").ok(); // Optional proxy for the Source API - let source_api = web::Data::new(SourceApi::new( - source_api_url, - source_api_key, - source_api_proxy_url, - )); - env_logger::init_from_env(Env::default().default_filter_or("info")); - - HttpServer::new(move || { - App::new() - .app_data(web::PayloadConfig::new(1024 * 1024 * 50)) - .app_data(source_api.clone()) - .app_data(web::Data::new(UserIdentity { api_key: None })) - .wrap( - // Configure CORS - Cors::default() - .allow_any_origin() - .allow_any_method() - .allow_any_header() - .supports_credentials() - .block_on_origin_mismatch(false) - .max_age(3600), - ) - .wrap(middleware::NormalizePath::trim()) - .wrap(middleware::DefaultHeaders::new().add(("X-Version", VERSION))) - .wrap(middleware::Logger::default()) - .wrap(LoadIdentity) - // Register the endpoints - .service(get_object) - .service(delete_object) - .service(post_handler) - .service(put_object) - .service(head_object) - .service(list_objects) - .service(index) - }) - .bind("0.0.0.0:8080")? - .run() - .await -} diff --git a/src/pagination.rs b/src/pagination.rs new file mode 100644 index 0000000..51b9d38 --- /dev/null +++ b/src/pagination.rs @@ -0,0 +1,50 @@ +use multistore::api::list::ListQueryParams; + +/// Result of applying pagination to a list of prefixes. +pub struct PaginatedPrefixes { + pub prefixes: Vec, + pub is_truncated: bool, + pub next_continuation_token: Option, +} + +/// Apply S3-style pagination to a sorted list of prefix strings. +/// +/// Prefixes are sorted lexicographically, then filtered by `start_after` / +/// `continuation_token`, and sliced to `max_keys`. +// TODO: Replace client-side pagination with real server-side pagination +// when the upstream Source Coop API supports paginated product listing. +pub fn paginate_prefixes(mut prefixes: Vec, params: &ListQueryParams) -> PaginatedPrefixes { + prefixes.sort(); + + // continuation-token takes precedence over start-after (per S3 spec) + let skip_after = params + .continuation_token + .as_deref() + .or(params.start_after.as_deref()); + + let iter: Box> = if let Some(after) = skip_after { + Box::new(prefixes.into_iter().filter(move |p| p.as_str() > after)) + } else { + Box::new(prefixes.into_iter()) + }; + + let collected: Vec = iter.take(params.max_keys + 1).collect(); + let is_truncated = collected.len() > params.max_keys; + + let mut result: Vec = collected; + if is_truncated { + result.truncate(params.max_keys); + } + + let next_continuation_token = if is_truncated { + result.last().cloned() + } else { + None + }; + + PaginatedPrefixes { + prefixes: result, + is_truncated, + next_continuation_token, + } +} diff --git a/src/registry.rs b/src/registry.rs new file mode 100644 index 0000000..e3ede4e --- /dev/null +++ b/src/registry.rs @@ -0,0 +1,284 @@ +//! Custom `BucketRegistry` that resolves product backends via source.coop API. + +use multistore::api::response::BucketEntry; +use multistore::error::ProxyError; +use multistore::registry::{BucketRegistry, ResolvedBucket}; +use multistore::types::{BucketConfig, ResolvedIdentity, S3Operation}; +use serde::Deserialize; +use std::collections::HashMap; + +/// Registry that resolves Source Cooperative products to multistore `BucketConfig`s +/// by calling the Source Cooperative API. +#[derive(Clone)] +pub struct SourceCoopRegistry { + api_base_url: String, + api_secret: Option, + request_id: String, +} + +impl SourceCoopRegistry { + pub fn new(api_base_url: String, api_secret: Option, request_id: String) -> Self { + Self { + api_base_url, + api_secret, + request_id, + } + } + + /// Parse "account--product" bucket name into (account, product). + fn parse_bucket_name(name: &str) -> Option<(&str, &str)> { + name.split_once("--") + } + + /// List products for an account via the Source API. + pub async fn list_products(&self, account: &str) -> Result, ProxyError> { + let product_list = crate::cache::get_or_fetch_product_list( + &self.api_base_url, + account, + self.api_secret.as_deref(), + &self.request_id, + ) + .await?; + Ok(product_list + .products + .into_iter() + .map(|p| p.product_id) + .collect()) + } +} + +impl BucketRegistry for SourceCoopRegistry { + async fn get_bucket( + &self, + name: &str, + _identity: &ResolvedIdentity, + _operation: &S3Operation, + ) -> Result { + let (account, product) = Self::parse_bucket_name(name) + .ok_or_else(|| ProxyError::BucketNotFound(name.to_string()))?; + + let config = resolve_product_send( + &self.api_base_url, + account, + product, + self.api_secret.as_deref(), + &self.request_id, + ) + .await?; + + Ok(ResolvedBucket { + config, + list_rewrite: None, + display_name: None, + }) + } + + async fn list_buckets( + &self, + _identity: &ResolvedIdentity, + ) -> Result, ProxyError> { + Ok(vec![]) + } +} + +/// Resolve a product to a BucketConfig, bridging the !Send worker::Fetch +/// into a Send future via spawn_local + oneshot channel. +async fn resolve_product_send( + api_base_url: &str, + account: &str, + product: &str, + api_secret: Option<&str>, + request_id: &str, +) -> Result { + let (tx, rx) = futures::channel::oneshot::channel(); + let api_base_url = api_base_url.to_string(); + let account = account.to_string(); + let product = product.to_string(); + let api_secret = api_secret.map(|s| s.to_string()); + let request_id = request_id.to_string(); + + wasm_bindgen_futures::spawn_local(async move { + let result = resolve_product_inner( + &api_base_url, + &account, + &product, + api_secret.as_deref(), + &request_id, + ) + .await; + let _ = tx.send(result); + }); + + rx.await + .unwrap_or_else(|_| Err(ProxyError::Internal("registry channel dropped".into()))) +} + +/// Inner product resolution logic (runs in spawn_local, !Send is OK). +async fn resolve_product_inner( + api_base_url: &str, + account: &str, + product: &str, + api_secret: Option<&str>, + request_id: &str, +) -> Result { + let span = tracing::info_span!( + "resolve_product", + account = %account, + product = %product, + backend_type = tracing::field::Empty, + ); + let _guard = span.enter(); + + // 1. Fetch product metadata + let source_product = + crate::cache::get_or_fetch_product(api_base_url, account, product, api_secret, request_id) + .await?; + + // 2. Find primary mirror + let primary_key = &source_product.metadata.primary_mirror; + let mirror = source_product + .metadata + .mirrors + .get(primary_key) + .or_else(|| source_product.metadata.mirrors.values().next()) + .ok_or_else(|| { + ProxyError::BucketNotFound(format!("no mirrors for {}/{}", account, product)) + })?; + + // 3. Fetch data connections to resolve the actual bucket + let connections = + crate::cache::get_or_fetch_data_connections(api_base_url, api_secret, request_id).await?; + + let connection = connections + .iter() + .find(|c| c.data_connection_id == mirror.connection_id) + .ok_or_else(|| { + ProxyError::Internal(format!( + "data connection '{}' not found", + mirror.connection_id + )) + })?; + + // 4. Build BucketConfig + let backend_type = match connection.details.provider.as_str() { + "s3" => "s3", + "az" | "azure" => "az", + "gcs" | "gs" => "gcs", + other => { + return Err(ProxyError::Internal(format!( + "unsupported provider: {}", + other + ))) + } + } + .to_string(); + + let mut backend_options = HashMap::new(); + + match backend_type.as_str() { + "s3" => { + if let Some(ref bucket) = connection.details.bucket { + backend_options.insert("bucket_name".to_string(), bucket.clone()); + } + if let Some(ref region) = connection.details.region { + backend_options.insert("region".to_string(), region.clone()); + backend_options.insert( + "endpoint".to_string(), + format!("https://s3.{}.amazonaws.com", region), + ); + } + } + "az" => { + if let Some(ref account_name) = connection.details.account_name { + backend_options.insert("account_name".to_string(), account_name.clone()); + } + if let Some(ref container) = connection.details.container_name { + backend_options.insert("container_name".to_string(), container.clone()); + } + } + _ => {} + } + + // Anonymous access — skip signing + backend_options.insert("skip_signature".to_string(), "true".to_string()); + + // 5. Build prefix: connection.base_prefix + mirror.prefix + let base_prefix = connection.details.base_prefix.as_deref().unwrap_or(""); + let mirror_prefix = &mirror.prefix; + let full_prefix = format!("{}{}", base_prefix, mirror_prefix); + let backend_prefix = if full_prefix.is_empty() { + None + } else { + Some(full_prefix) + }; + + let config = BucketConfig { + name: format!("{}--{}", account, product), + backend_type, + backend_prefix, + anonymous_access: true, + allowed_roles: vec![], + backend_options, + }; + + span.record("backend_type", config.backend_type.as_str()); + tracing::debug!( + prefix = ?config.backend_prefix, + options = ?config.backend_options, + "product resolved", + ); + + Ok(config) +} + +// ── API response types ───────────────────────────────────────────── + +#[derive(Debug, Clone, Deserialize)] +pub struct SourceProduct { + pub product_id: String, + pub metadata: SourceProductMetadata, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct SourceProductMetadata { + pub mirrors: HashMap, + pub primary_mirror: String, +} + +#[derive(Debug, Clone, Deserialize)] +#[allow(dead_code)] +pub struct SourceProductMirror { + pub storage_type: String, + pub connection_id: String, + pub prefix: String, + pub config: SourceProductMirrorConfig, + pub is_primary: bool, +} + +#[derive(Debug, Clone, Deserialize)] +#[allow(dead_code)] +pub struct SourceProductMirrorConfig { + pub region: Option, + pub bucket: Option, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct DataConnection { + pub data_connection_id: String, + pub details: DataConnectionDetails, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct DataConnectionDetails { + pub provider: String, + pub bucket: Option, + pub region: Option, + pub base_prefix: Option, + pub account_name: Option, + pub container_name: Option, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct SourceProductList { + pub products: Vec, +} diff --git a/src/routing.rs b/src/routing.rs new file mode 100644 index 0000000..5556f52 --- /dev/null +++ b/src/routing.rs @@ -0,0 +1,319 @@ +use multistore_path_mapping::PathMapping; + +#[derive(Debug, PartialEq)] +pub enum RequestClass { + /// Root index: `GET /` + Index, + /// Bad request + BadRequest(String), + /// List products for an account: `GET /{account}?list-type=2` (no product prefix) + AccountList { + account: String, + query: Option, + }, + /// Everything else goes through the gateway with a rewritten path + ProxyRequest { + rewritten_path: String, + query: Option, + }, +} + +/// Classify an incoming request into one of the handled cases. +pub fn classify_request(mapping: &PathMapping, path: &str, query: Option<&str>) -> RequestClass { + let trimmed = path.trim_matches('/'); + + // Root + if trimmed.is_empty() { + return RequestClass::Index; + } + + // Try mapping the path (works for /{account}/{product}[/{key}]) + if let Some(mapped) = mapping.parse(path) { + let rewritten_path = match mapped.key { + Some(ref key) => format!("/{}/{}", mapped.bucket, key), + None => format!("/{}", mapped.bucket), + }; + return RequestClass::ProxyRequest { + rewritten_path, + query: query.map(|q| q.to_string()), + }; + } + + // Single segment: /{account} — must be a list or prefix-routed request + let segments: Vec<&str> = trimmed.splitn(2, '/').collect(); + if segments.len() == 1 { + let account = segments[0]; + let query_str = query.unwrap_or(""); + + if is_list_request(query_str) { + if let Some(prefix) = extract_query_param(query_str, "prefix") { + if !prefix.is_empty() { + return route_list_with_prefix(mapping, account, &prefix, query_str); + } + } + // No prefix — list products for this account + return RequestClass::AccountList { + account: account.to_string(), + query: if query_str.is_empty() { + None + } else { + Some(query_str.to_string()) + }, + }; + } + return RequestClass::BadRequest("Missing product in path".to_string()); + } + + // Shouldn't reach here, but fall back to bad request + RequestClass::BadRequest("Invalid request".to_string()) +} + +/// Route a list request where the prefix contains a product name. +/// +/// `GET /{account}?list-type=2&prefix=product/subdir/` becomes +/// a proxy request to `/{account--product}?list-type=2&prefix=subdir/`. +fn route_list_with_prefix( + mapping: &PathMapping, + account: &str, + prefix: &str, + query_str: &str, +) -> RequestClass { + let (product, remaining_prefix) = if let Some(slash_pos) = prefix.find('/') { + (&prefix[..slash_pos], &prefix[slash_pos + 1..]) + } else { + (prefix, "") + }; + + let bucket = format!("{}{}{}", account, mapping.bucket_separator, product); + let new_query = rewrite_prefix_in_query(query_str, remaining_prefix); + + RequestClass::ProxyRequest { + rewritten_path: format!("/{}", bucket), + query: Some(new_query), + } +} + +pub fn is_list_request(query: &str) -> bool { + query.contains("list-type=") +} + +pub fn extract_query_param(query: &str, key: &str) -> Option { + query.split('&').find_map(|pair| { + pair.split_once('=') + .filter(|(k, _)| *k == key) + .map(|(_, v)| { + percent_encoding::percent_decode_str(v) + .decode_utf8_lossy() + .into_owned() + }) + }) +} + +/// Characters that must be percent-encoded when placed in a query parameter value. +/// Only encodes characters that would break query-string parsing or URL structure. +const QUERY_VALUE_ENCODE: &percent_encoding::AsciiSet = &percent_encoding::CONTROLS + .add(b' ') + .add(b'#') + .add(b'&') + .add(b'=') + .add(b'+'); + +pub fn rewrite_prefix_in_query(query: &str, new_prefix: &str) -> String { + let encoded: String = + percent_encoding::utf8_percent_encode(new_prefix, QUERY_VALUE_ENCODE).to_string(); + query + .split('&') + .map(|pair| { + if pair.starts_with("prefix=") { + format!("prefix={}", encoded) + } else { + pair.to_string() + } + }) + .collect::>() + .join("&") +} + +#[cfg(test)] +mod tests { + use super::*; + + fn mapping() -> PathMapping { + PathMapping { + bucket_segments: 2, + bucket_separator: "--".to_string(), + display_bucket_segments: 1, + } + } + + #[test] + fn test_root_path() { + assert_eq!(classify_request(&mapping(), "/", None), RequestClass::Index); + } + + #[test] + fn test_root_empty() { + assert_eq!(classify_request(&mapping(), "", None), RequestClass::Index); + } + + #[test] + fn test_object_request() { + let result = classify_request(&mapping(), "/cholmes/admin-boundaries/file.parquet", None); + assert_eq!( + result, + RequestClass::ProxyRequest { + rewritten_path: "/cholmes--admin-boundaries/file.parquet".to_string(), + query: None, + } + ); + } + + #[test] + fn test_object_request_nested_key() { + let result = classify_request( + &mapping(), + "/cholmes/admin-boundaries/dir/subdir/file.parquet", + None, + ); + assert_eq!( + result, + RequestClass::ProxyRequest { + rewritten_path: "/cholmes--admin-boundaries/dir/subdir/file.parquet".to_string(), + query: None, + } + ); + } + + #[test] + fn test_product_list_via_segment() { + let result = classify_request(&mapping(), "/cholmes/admin-boundaries", Some("list-type=2")); + assert_eq!( + result, + RequestClass::ProxyRequest { + rewritten_path: "/cholmes--admin-boundaries".to_string(), + query: Some("list-type=2".to_string()), + } + ); + } + + #[test] + fn test_account_list() { + let result = classify_request(&mapping(), "/cholmes", Some("list-type=2")); + assert_eq!( + result, + RequestClass::AccountList { + account: "cholmes".to_string(), + query: Some("list-type=2".to_string()), + } + ); + } + + #[test] + fn test_account_list_trailing_slash() { + let result = classify_request(&mapping(), "/cholmes/", Some("list-type=2")); + assert_eq!( + result, + RequestClass::AccountList { + account: "cholmes".to_string(), + query: Some("list-type=2".to_string()), + } + ); + } + + #[test] + fn test_prefix_routed_list() { + let result = classify_request( + &mapping(), + "/cholmes", + Some("list-type=2&prefix=admin-boundaries/"), + ); + assert_eq!( + result, + RequestClass::ProxyRequest { + rewritten_path: "/cholmes--admin-boundaries".to_string(), + query: Some("list-type=2&prefix=".to_string()), + } + ); + } + + #[test] + fn test_prefix_routed_list_with_subdir() { + let result = classify_request( + &mapping(), + "/cholmes", + Some("list-type=2&prefix=admin-boundaries/subdir/"), + ); + assert_eq!( + result, + RequestClass::ProxyRequest { + rewritten_path: "/cholmes--admin-boundaries".to_string(), + query: Some("list-type=2&prefix=subdir/".to_string()), + } + ); + } + + #[test] + fn test_single_segment_no_list() { + let result = classify_request(&mapping(), "/cholmes", None); + assert_eq!( + result, + RequestClass::BadRequest("Missing product in path".to_string()) + ); + } + + #[test] + fn test_url_encoded_prefix() { + let result = classify_request( + &mapping(), + "/cholmes", + Some("list-type=2&prefix=admin%20boundaries/subdir/"), + ); + assert_eq!( + result, + RequestClass::ProxyRequest { + rewritten_path: "/cholmes--admin boundaries".to_string(), + query: Some("list-type=2&prefix=subdir/".to_string()), + } + ); + } + + // ── Query helper tests ────────────────────────────────────────── + + #[test] + fn test_is_list_request() { + assert!(is_list_request("list-type=2")); + assert!(is_list_request("foo=bar&list-type=2&baz=qux")); + assert!(!is_list_request("foo=bar")); + assert!(!is_list_request("")); + } + + #[test] + fn test_extract_query_param() { + assert_eq!( + extract_query_param("list-type=2&prefix=foo/", "prefix"), + Some("foo/".to_string()) + ); + assert_eq!(extract_query_param("list-type=2", "prefix"), None); + assert_eq!( + extract_query_param("prefix=hello%20world", "prefix"), + Some("hello world".to_string()) + ); + } + + #[test] + fn test_rewrite_prefix_in_query() { + assert_eq!( + rewrite_prefix_in_query("list-type=2&prefix=old/", "new/"), + "list-type=2&prefix=new/" + ); + assert_eq!( + rewrite_prefix_in_query("prefix=old/&max-keys=100", ""), + "prefix=&max-keys=100" + ); + // Only query-breaking characters are percent-encoded (spaces, &, =, #, +) + assert_eq!( + rewrite_prefix_in_query("list-type=2&prefix=old/", "sub dir/"), + "list-type=2&prefix=sub%20dir/" + ); + } +} diff --git a/src/utils/api.rs b/src/utils/api.rs deleted file mode 100644 index 708721b..0000000 --- a/src/utils/api.rs +++ /dev/null @@ -1,45 +0,0 @@ -use crate::utils::errors::BackendError; -use reqwest::{Response, StatusCode}; -use serde::de::DeserializeOwned; - -/// Process a response, handling both success and error cases -pub async fn process_json_response( - response: Response, - not_found_error: BackendError, -) -> Result { - let status = response.status(); - let url = response.url().to_string(); - let text = response - .text() - .await - .unwrap_or_else(|_| "".to_string()); - - if status.is_success() { - match serde_json::from_str::(&text) { - Ok(val) => Ok(val), - Err(err) => { - log::error!("Failed to parse JSON from {}: {}\nBody: {}", url, err, text); - Err(BackendError::JsonParseError { url }) - } - } - } else if status == StatusCode::NOT_FOUND { - Err(not_found_error) - } else { - let is_server_error = status.is_server_error(); - if is_server_error { - log::error!("Server error from {}: {}\nBody: {}", url, status, text); - Err(BackendError::ApiServerError { - url, - status: status.as_u16(), - message: text, - }) - } else { - log::warn!("Client error from {}: {}\nBody: {}", url, status, text); - Err(BackendError::ApiClientError { - url, - status: status.as_u16(), - message: text, - }) - } - } -} diff --git a/src/utils/auth.rs b/src/utils/auth.rs deleted file mode 100644 index dd6b8c2..0000000 --- a/src/utils/auth.rs +++ /dev/null @@ -1,505 +0,0 @@ -use actix_http::header::HeaderMap; -use actix_web::{ - dev::{self, Service, ServiceRequest, ServiceResponse, Transform}, - web, - web::BytesMut, - Error, HttpMessage, -}; -use futures_util::{future::LocalBoxFuture, stream::StreamExt}; -use hex; -use hmac::{Hmac, Mac}; -use percent_encoding::percent_decode_str; -use sha2::{Digest, Sha256}; -use std::{ - borrow::Cow, - collections::BTreeMap, - future::{ready, Ready}, - rc::Rc, -}; -use url::form_urlencoded; - -use crate::apis::source::{APIKey, SourceApi}; -use crate::utils::errors::BackendError; -use async_trait::async_trait; - -#[async_trait] -pub trait ApiKeyProvider: Send + Sync { - async fn get_api_key(&self, access_key_id: &str) -> Result; -} - -#[async_trait] -impl ApiKeyProvider for SourceApi { - async fn get_api_key(&self, access_key_id: &str) -> Result { - self.get_api_key(access_key_id).await - } -} - -#[derive(Clone)] -pub struct UserIdentity { - pub api_key: Option, -} - -pub struct LoadIdentity; - -impl Transform for LoadIdentity -where - S: Service, Error = Error>, - S::Future: 'static, - B: 'static, -{ - type Response = ServiceResponse; - type Error = Error; - type InitError = (); - type Transform = LoadIdentityMiddleware; - type Future = Ready>; - - fn new_transform(&self, service: S) -> Self::Future { - ready(Ok(LoadIdentityMiddleware { - service: Rc::new(service), - })) - } -} - -pub struct LoadIdentityMiddleware { - service: Rc, -} - -impl Service for LoadIdentityMiddleware -where - S: Service, Error = Error> + 'static, - S::Future: 'static, - B: 'static, -{ - type Response = ServiceResponse; - type Error = Error; - type Future = LocalBoxFuture<'static, Result>; - - dev::forward_ready!(service); - - fn call(&self, mut req: ServiceRequest) -> Self::Future { - let svc = self.service.clone(); - - Box::pin(async move { - let mut body = BytesMut::new(); - let mut stream = req.take_payload(); - while let Some(chunk) = stream.next().await { - body.extend_from_slice(&chunk?); - } - - let identity = match load_identity( - req.app_data::>().unwrap(), - req.method().as_str(), - req.path(), - req.headers(), - req.query_string(), - &body, - ) - .await - { - Ok(api_key) => UserIdentity { - api_key: Some(api_key), - }, - Err(_) => UserIdentity { api_key: None }, - }; - - req.extensions_mut().insert(identity); - - let (_, mut payload) = actix_http::h1::Payload::create(true); - - payload.unread_data(body.into()); - req.set_payload(payload.into()); - - let res = svc.call(req).await?; - - Ok(res) - }) - } -} - -async fn load_identity( - source_api: &web::Data, - method: &str, - path: &str, - headers: &HeaderMap, - query_string: &str, - body: &BytesMut, -) -> Result -where - T: ApiKeyProvider, -{ - let Some(auth) = headers.get("Authorization") else { - return Err("No Authorization header found".to_string()); - }; - - let authorization_header = auth.to_str().unwrap(); - let signature_method = authorization_header.split(" ").next().unwrap(); - - if signature_method != "AWS4-HMAC-SHA256" { - return Err("Invalid Signature Algorithm".to_string()); - } - - let parts = authorization_header - .split(",") - .map(|part| part.trim()) - .collect::>(); - - let credential = parts[0].split("Credential=").nth(1).unwrap_or(""); - let signed_headers = parts[1] - .split("SignedHeaders=") - .nth(1) - .unwrap_or("") - .split(";") - .collect(); - let signature = parts[2].split("Signature=").nth(1).unwrap_or(""); - - let parts = credential.split("/").collect::>(); - let access_key_id = parts[0]; - let date = parts[1]; - let region = parts[2]; - let service = parts[3]; - - let Some(content_hash) = headers.get("x-amz-content-sha256") else { - return Err("No x-amz-content-sha256 header found".to_string()); - }; - - let canonical_request = create_canonical_request( - method, - path, - headers, - signed_headers, - query_string, - body, - content_hash.to_str().unwrap(), - ); - let credential_scope = format!("{date}/{region}/{service}/aws4_request"); - - let Some(datetime) = headers.get("x-amz-date") else { - return Err("No x-amz-date header found".to_string()); - }; - - let api_key = source_api - .get_api_key(access_key_id) - .await - .map_err(|e| e.to_string())?; - - let string_to_sign = create_string_to_sign( - &canonical_request, - datetime.to_str().unwrap(), - &credential_scope, - ); - - let calculated_signature = calculate_signature( - api_key.secret_access_key.as_str(), - date, - region, - service, - &string_to_sign, - ); - - if calculated_signature != signature { - Err("Signature mismatch".to_string()) - } else { - Ok(api_key) - } -} - -fn uri_encode(input: &str, encode_forward_slash: bool) -> Cow<'_, str> { - let mut encoded = String::new(); - let chars = input.chars().peekable(); - - for ch in chars { - if (ch == '/' && !encode_forward_slash) - || (ch.is_ascii_alphanumeric() || ch == '-' || ch == '_' || ch == '.' || ch == '~') - { - encoded.push(ch); - } else { - for byte in ch.to_string().as_bytes() { - encoded.push_str(&format!("%{byte:02X}")); - } - } - } - - if encoded == input { - Cow::Borrowed(input) - } else { - Cow::Owned(encoded) - } -} - -fn trim(input: &str) -> String { - input.trim().to_string() -} - -fn lowercase(input: &str) -> String { - input.to_lowercase() -} - -fn hmac_sha256(key: &[u8], message: &[u8]) -> Vec { - // Create HMAC-SHA256 instance - let mut mac = Hmac::::new_from_slice(key).expect("HMAC can take key of any size"); - - // Add message to HMAC - mac.update(message); - - // Calculate HMAC - let result = mac.finalize(); - - // Get the result as bytes - result.into_bytes().to_vec() -} - -fn calculate_signature( - key: &str, - date: &str, - region: &str, - service: &str, - string_to_sign: &str, -) -> String { - let k_date = hmac_sha256(format!("AWS4{key}").as_bytes(), date.as_bytes()); - let k_region = hmac_sha256(&k_date, region.as_bytes()); - let k_service = hmac_sha256(&k_region, service.as_bytes()); - let k_signing = hmac_sha256(&k_service, b"aws4_request"); - - hex::encode(hmac_sha256(&k_signing, string_to_sign.as_bytes())) -} - -fn create_string_to_sign( - canonical_request: &str, - datetime: &str, - credential_scope: &str, -) -> String { - format!( - "AWS4-HMAC-SHA256\n{}\n{}\n{}", - datetime, - credential_scope, - hex::encode(Sha256::digest(canonical_request.as_bytes())) - ) -} - -fn create_canonical_request( - method: &str, - path: &str, - headers: &HeaderMap, - signed_headers: Vec<&str>, - query_string: &str, - body: &BytesMut, - content_hash: &str, -) -> String { - let decoded_path = percent_decode_str(path).decode_utf8().unwrap(); - if content_hash == "UNSIGNED-PAYLOAD" { - return format!( - "{}\n{}\n{}\n{}\n{}\n{}", - method, - uri_encode(decoded_path.as_ref(), false), - get_canonical_query_string(query_string), - get_canonical_headers(headers, &signed_headers), - get_signed_headers(&signed_headers), - content_hash - ); - } - format!( - "{}\n{}\n{}\n{}\n{}\n{}", - method, - uri_encode(decoded_path.as_ref(), false), - get_canonical_query_string(query_string), - get_canonical_headers(headers, &signed_headers), - get_signed_headers(&signed_headers), - hash_payload(body) - ) -} - -fn get_canonical_query_string(query_string: &str) -> String { - if query_string.is_empty() { - return String::new(); - } - - let parsed: Vec<(String, String)> = form_urlencoded::parse(query_string.as_bytes()) - .map(|(key, value)| (key.to_string(), value.to_string())) - .collect(); - - let mut sorted_params: Vec<(String, String)> = parsed; - sorted_params.sort_by(|a, b| a.0.cmp(&b.0)); - - let mut encoded_params: Vec = Vec::new(); - - for (key, value) in sorted_params { - let encoded_key = uri_encode(&key, true); - let encoded_value = uri_encode(&value, true); - - encoded_params.push(format!("{encoded_key}={encoded_value}")); - } - - encoded_params.join("&") -} - -fn get_canonical_headers(headers: &HeaderMap, signed_headers: &Vec<&str>) -> String { - let mut canonical_headers = BTreeMap::new(); - - for (name, value) in headers.iter() { - let canonical_name = lowercase(name.as_str()); - let canonical_value = trim(value.to_str().unwrap()); - - if signed_headers.contains(&canonical_name.as_str()) { - canonical_headers - .entry(canonical_name) - .or_insert_with(Vec::new) - .push(canonical_value); - } - } - - canonical_headers - .iter() - .fold(String::new(), |mut output, (name, values)| { - output.push_str(&format!("{}:{}\n", name, values.join(","))); - output - }) -} - -fn get_signed_headers(signed_headers: &Vec<&str>) -> String { - signed_headers - .iter() - .map(|header| lowercase(header)) - .collect::>() - .join(";") -} - -fn hash_payload(body: &BytesMut) -> String { - hex::encode(Sha256::digest(body)) -} - -#[cfg(test)] -mod tests { - use super::*; - use actix_http::header::{HeaderMap, HeaderName, HeaderValue}; - use async_trait::async_trait; - use common_s3_headers::S3HeadersBuilder; - use std::str::FromStr; - use url::Url; - - #[derive(Clone)] - struct TestSourceApi { - api_key: Option, - } - - impl TestSourceApi { - fn new(api_key: Option) -> Self { - Self { api_key } - } - } - - #[async_trait] - impl ApiKeyProvider for TestSourceApi { - async fn get_api_key(&self, _access_key_id: &str) -> Result { - let Some(key) = &self.api_key else { - return Err(BackendError::ApiKeyNotFound); - }; - Ok(key.clone()) - } - } - - fn create_test_source_api(api_key: Option) -> web::Data { - web::Data::new(TestSourceApi::new(api_key)) - } - - #[tokio::test] - async fn test_load_identity_missing_auth_header() { - let headers = HeaderMap::new(); - let source_api = create_test_source_api(None); - - let result = - load_identity(&source_api, "GET", "/test", &headers, "", &BytesMut::new()).await; - - assert!(result.is_err()); - assert_eq!(result.unwrap_err(), "No Authorization header found"); - } - - #[tokio::test] - async fn test_load_identity_invalid_signature_method() { - let mut headers = HeaderMap::new(); - headers.insert( - HeaderName::from_str("Authorization").unwrap(), - HeaderValue::from_str("INVALID Credential=test-key/20240315/us-east-1/s3, SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature=test-signature").unwrap(), - ); - - let source_api = create_test_source_api(None); - - let result = - load_identity(&source_api, "GET", "/test", &headers, "", &BytesMut::new()).await; - - assert!(result.is_err()); - assert_eq!(result.unwrap_err(), "Invalid Signature Algorithm"); - } - - #[tokio::test] - async fn test_load_identity_missing_content_hash() { - let mut headers = HeaderMap::new(); - headers.insert( - HeaderName::from_str("Authorization").unwrap(), - HeaderValue::from_str("AWS4-HMAC-SHA256 Credential=test-key/20240315/us-east-1/s3, SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature=test-signature").unwrap(), - ); - - let source_api = create_test_source_api(None); - - let result = - load_identity(&source_api, "GET", "/test", &headers, "", &BytesMut::new()).await; - - assert!(result.is_err()); - assert_eq!(result.unwrap_err(), "No x-amz-content-sha256 header found"); - } - - #[tokio::test] - async fn test_load_identity_missing_date() { - let mut headers = HeaderMap::new(); - headers.insert( - HeaderName::from_str("Authorization").unwrap(), - HeaderValue::from_str("AWS4-HMAC-SHA256 Credential=test-key/20240315/us-east-1/s3, SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature=test-signature").unwrap(), - ); - headers.insert( - HeaderName::from_str("x-amz-content-sha256").unwrap(), - HeaderValue::from_str("test-hash").unwrap(), - ); - - let source_api = create_test_source_api(None); - - let result = - load_identity(&source_api, "GET", "/test", &headers, "", &BytesMut::new()).await; - - assert!(result.is_err()); - assert_eq!(result.unwrap_err(), "No x-amz-date header found"); - } - - #[tokio::test] - async fn test_load_identity_success() { - let api_key = APIKey { - access_key_id: "test-key".to_string(), - secret_access_key: "test-secret".to_string(), - }; - let source_api = create_test_source_api(Some(api_key.clone())); - - let method = "GET"; - let url = Url::parse("https://test.com/test").unwrap(); - let path = url.path(); - - let headers = HeaderMap::from_iter( - S3HeadersBuilder::new(&url) - .set_access_key(api_key.access_key_id.as_str()) - .set_secret_key(api_key.secret_access_key.as_str()) - .set_region("us-east-1") - .set_method("GET") - .set_service("s3") - .build() - .iter() - .map(|(k, v)| { - ( - HeaderName::from_str(k).unwrap(), - HeaderValue::from_str(v.as_str()).unwrap(), - ) - }), - ); - - let result = load_identity(&source_api, method, path, &headers, "", &BytesMut::new()).await; - - assert!(result.is_ok()); - assert_eq!(result.unwrap().access_key_id, "test-key"); - } -} diff --git a/src/utils/core.rs b/src/utils/core.rs deleted file mode 100644 index 723c6dc..0000000 --- a/src/utils/core.rs +++ /dev/null @@ -1,246 +0,0 @@ -use actix_web::{ - body::{BodySize, MessageBody}, - web, Error as ActixError, -}; -use futures::Stream; -use pin_project_lite::pin_project; -use std::task::{Context, Poll}; -use std::{pin::Pin, str::FromStr}; - -pin_project! { - pub struct StreamingResponse { - #[pin] - inner: S, - size: u64, - } -} - -impl StreamingResponse { - pub fn new(inner: S, size: u64) -> Self { - Self { inner, size } - } -} - -impl MessageBody for StreamingResponse -where - S: Stream, - S::Item: Into>, -{ - type Error = ActixError; - - fn size(&self) -> BodySize { - BodySize::Sized(self.size) - } - - fn poll_next( - self: Pin<&mut Self>, - cx: &mut Context<'_>, - ) -> Poll>> { - let this = self.project(); - match this.inner.poll_next(cx) { - Poll::Ready(Some(item)) => Poll::Ready(Some(item.into())), - Poll::Ready(None) => Poll::Ready(None), - Poll::Pending => Poll::Pending, - } - } -} - -pub fn replace_first(original: String, from: String, to: String) -> String { - match original.find(&from) { - Some(start_index) => { - let mut result = String::with_capacity(original.len()); - result.push_str(&original[..start_index]); - result.push_str(&to); - result.push_str(&original[start_index + from.len()..]); - result - } - None => original, - } -} - -/// Splits a string at the first forward slash ('/') character. -/// -/// This function takes a string as input and returns a tuple of two strings. -/// The first string in the tuple contains the part of the input before the -/// first slash, and the second string contains the part after the first slash. -/// -/// # Arguments -/// -/// * `input` - A String that may or may not contain a forward slash. -/// -/// # Returns -/// -/// A tuple `(String, String)` where: -/// - The first element is the substring before the first slash. -/// - The second element is the substring after the first slash. -/// -/// If there is no slash in the input string, the function returns the entire -/// input as the first element of the tuple and an empty string as the second element. -/// -/// # Examples -/// -/// ``` -/// let (before, after) = split_at_first_slash("path/to/file".to_string()); -/// assert_eq!(before, "path"); -/// assert_eq!(after, "to/file"); -/// -/// let (before, after) = split_at_first_slash("no_slash".to_string()); -/// assert_eq!(before, "no_slash"); -/// assert_eq!(after, ""); -/// ``` -pub fn split_at_first_slash(input: &str) -> (&str, &str) { - match input.find('/') { - Some(index) => { - let (before, after) = input.split_at(index); - (before, &after[1..]) - } - None => (input, ""), - } -} - -/// Parsed range request information. -#[derive(Debug, Clone, Copy, PartialEq)] -pub struct RangeRequest { - /// The start byte offset - pub start: u64, - /// The end byte offset (inclusive), or `None` for open-ended ranges (e.g. "bytes=100-") - pub end: Option, -} - -impl From for String { - fn from(r: RangeRequest) -> Self { - match r.end { - Some(end) => format!("bytes={}-{}", r.start, end), - None => format!("bytes={}-", r.start), - } - } -} - -impl FromStr for RangeRequest { - type Err = (); - - fn from_str(s: &str) -> Result { - let bytes_range = s.strip_prefix("bytes=").ok_or(())?; - let (start_str, end_str) = bytes_range.split_once('-').ok_or(())?; - let start = start_str.parse::().map_err(|_| ())?; - - let end = if end_str.is_empty() { - None - } else { - let end = end_str.parse::().map_err(|_| ())?; - if start > end { - return Err(()); - } - Some(end) - }; - - Ok(RangeRequest { start, end }) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_parse_range_full_range() { - let result: RangeRequest = "bytes=0-1023".parse().unwrap(); - assert_eq!( - result, - RangeRequest { - start: 0, - end: Some(1023), - } - ); - } - - #[test] - fn test_parse_range_open_ended() { - let result = "bytes=100-".parse::().unwrap(); - assert_eq!( - result, - RangeRequest { - start: 100, - end: None, - } - ); - } - - #[test] - fn test_parse_range_open_ended_no_total_size() { - // Without with_total_size, end remains None - let result: RangeRequest = "bytes=100-".parse().unwrap(); - assert_eq!(result.end, None); - } - - #[test] - fn test_parse_range_missing_prefix() { - assert!("invalid=0-100".parse::().is_err()); - } - - #[test] - fn test_parse_range_non_numeric_start() { - assert!("bytes=abc-100".parse::().is_err()); - } - - #[test] - fn test_parse_range_non_numeric_end() { - assert!("bytes=0-abc".parse::().is_err()); - } - - #[test] - fn test_parse_range_start_greater_than_end() { - assert!("bytes=500-100".parse::().is_err()); - } - - #[test] - fn test_parse_range_start_beyond_total_size() { - // Parsing succeeds; validation against total_size is the caller's responsibility - let result: RangeRequest = "bytes=1000-1023".parse().unwrap(); - assert_eq!(result.start, 1000); - assert_eq!(result.end, Some(1023)); - } - - #[test] - fn test_parse_range_single_byte() { - let result: RangeRequest = "bytes=0-0".parse().unwrap(); - assert_eq!( - result, - RangeRequest { - start: 0, - end: Some(0), - } - ); - } - - #[test] - fn test_parse_range_large_file() { - let rr: RangeRequest = "bytes=0-1023".parse().unwrap(); - assert_eq!(rr.start, 0); - assert_eq!(rr.end, Some(1023)); - assert_eq!(rr.end.unwrap() - rr.start + 1, 1024); - } - - #[test] - fn test_parse_range_no_hyphen() { - assert!("bytes=100".parse::().is_err()); - } - - #[test] - fn test_parse_range_content_length_calculation() { - let result: RangeRequest = "bytes=0-1023".parse().unwrap(); - assert_eq!(result.end.unwrap() - result.start + 1, 1024); - } - - #[test] - fn test_parse_range_content_range_format() { - let result: RangeRequest = "bytes=0-1023".parse().unwrap(); - let content_range = format!( - "bytes {}-{}/{}", - result.start, - result.end.unwrap(), - 3515053862u64 - ); - assert_eq!(content_range, "bytes 0-1023/3515053862"); - } -} diff --git a/src/utils/errors.rs b/src/utils/errors.rs deleted file mode 100644 index 3adbc9a..0000000 --- a/src/utils/errors.rs +++ /dev/null @@ -1,654 +0,0 @@ -use actix_web::error; -use actix_web::http::StatusCode; -use actix_web::HttpResponse; -use azure_core::{ - error::{Error as AzureError, ErrorKind as AzureErrorKind}, - StatusCode as AzureStatusCode, -}; -use log::error; -use quick_xml::DeError; -use reqwest::Error as ReqwestError; -use rusoto_core::RusotoError; -use rusoto_s3::{ - AbortMultipartUploadError, CompleteMultipartUploadError, CreateMultipartUploadError, - DeleteObjectError, HeadObjectError, ListObjectsV2Error, PutObjectError, UploadPartError, -}; -use thiserror::Error; - -#[derive(Error, Debug)] -pub enum BackendError { - #[error("repository not found")] - RepositoryNotFound, - - #[error("failed to fetch repository permissions")] - RepositoryPermissionsNotFound, - - #[error("source repository missing primary mirror")] - SourceRepositoryMissingPrimaryMirror, - - #[error("object not found: {}", .0.clone().unwrap_or_default())] - ObjectNotFound(Option), - - #[error("api key not found")] - ApiKeyNotFound, - - #[error("data connection not found")] - DataConnectionNotFound, - - #[error("invalid request")] - InvalidRequest(String), - - #[error("reqwest error (url {}, message {})", .0.url().map(|u| u.to_string()).unwrap_or("unknown".to_string()), .0.to_string())] - ReqwestError(#[from] ReqwestError), - - #[error("api threw a server error (url {}, status {}, message {})", .url, .status, .message)] - ApiServerError { - url: String, - status: u16, - message: String, - }, - - #[error("api threw a client error (url {}, status {}, message {})", .url, .status, .message)] - ApiClientError { - url: String, - status: u16, - message: String, - }, - - #[error("failed to parse JSON (url {})", .url)] - JsonParseError { url: String }, - - #[error("unexpected data connection provider (provider {})", .provider)] - UnexpectedDataConnectionProvider { provider: String }, - - #[error("unauthorized")] - UnauthorizedError, - - #[error("unexpected API error: {0}")] - UnexpectedApiError(String), - - #[error("unsupported auth method: {0}")] - UnsupportedAuthMethod(String), - - #[error("unsupported operation: {0}")] - UnsupportedOperation(String), - - #[error("xml parse error: {0}")] - XmlParseError(String), - - #[error("azure error: {0}")] - AzureError(AzureError), - - #[error("s3 error: {0}")] - S3Error(String), -} - -impl error::ResponseError for BackendError { - fn error_response(&self) -> HttpResponse { - let status_code = self.status_code(); - let body = match status_code { - e if e.is_client_error() => self.to_string(), - _ => format!("Internal Server Error: {self}"), - }; - if status_code.is_server_error() { - error!("Error: {}", self); - } - HttpResponse::build(status_code).body(body) - } - - fn status_code(&self) -> StatusCode { - match self { - // Pass through client error status codes - BackendError::ApiClientError { status, .. } => { - StatusCode::from_u16(*status).unwrap_or(StatusCode::BAD_REQUEST) - } - - // 400 - BackendError::InvalidRequest(_) - | BackendError::UnsupportedAuthMethod(_) - | BackendError::UnsupportedOperation(_) => StatusCode::BAD_REQUEST, - // 401 - BackendError::UnauthorizedError => StatusCode::UNAUTHORIZED, - // 404 - BackendError::RepositoryNotFound - | BackendError::ObjectNotFound(_) - | BackendError::SourceRepositoryMissingPrimaryMirror - | BackendError::ApiKeyNotFound - | BackendError::DataConnectionNotFound => StatusCode::NOT_FOUND, - - // 502 - BackendError::ReqwestError(_) - | BackendError::ApiServerError { .. } - | BackendError::RepositoryPermissionsNotFound - | BackendError::AzureError(_) - | BackendError::S3Error(_) => StatusCode::BAD_GATEWAY, - // 500 - _ => StatusCode::INTERNAL_SERVER_ERROR, - } - } -} - -// Azure API Errors -impl From for BackendError { - fn from(error: AzureError) -> BackendError { - match error.kind() { - AzureErrorKind::HttpResponse { status, error_code } - if *status == AzureStatusCode::NotFound => - { - BackendError::ObjectNotFound(error_code.clone()) - } - _ => BackendError::AzureError(error), - } - } -} - -// S3 API Errors -fn get_rusoto_error_message( - operation: &str, - error: RusotoError, -) -> String { - match error { - RusotoError::Service(e) => format!("{operation} Service Error: {e}"), - RusotoError::HttpDispatch(e) => format!("{operation} HttpDispatch Error: {e}"), - RusotoError::Credentials(e) => format!("{operation} Credentials Error: {e}"), - RusotoError::Validation(e) => format!("{operation} Validation Error: {e}"), - RusotoError::ParseError(e) => format!("{operation} Parse Error: {e}"), - RusotoError::Unknown(e) => format!("{} Unknown Error: status {}", operation, e.status), - RusotoError::Blocking => format!("{operation} Blocking Error"), - } -} -macro_rules! impl_s3_errors { - ($(($error_type:ty, $operation:expr)),* $(,)?) => { - $( - impl From> for BackendError { - fn from(error: RusotoError<$error_type>) -> BackendError { - BackendError::S3Error(get_rusoto_error_message($operation, error)) - } - } - )* - }; -} -impl_s3_errors!( - (DeleteObjectError, "DeleteObject"), - (PutObjectError, "PutObject"), - (CreateMultipartUploadError, "CreateMultipartUpload"), - (AbortMultipartUploadError, "AbortMultipartUpload"), - (CompleteMultipartUploadError, "CompleteMultipartUpload"), - (UploadPartError, "UploadPart"), -); -impl From> for BackendError { - fn from(error: RusotoError) -> BackendError { - match error { - RusotoError::Service(HeadObjectError::NoSuchKey(e)) => { - BackendError::ObjectNotFound(Some(e)) - } - RusotoError::Unknown(e) if e.status == StatusCode::NOT_FOUND => { - BackendError::ObjectNotFound(Some(e.body_as_str().to_string())) - } - _ => BackendError::S3Error(get_rusoto_error_message("HeadObject", error)), - } - } -} -impl From> for BackendError { - fn from(error: RusotoError) -> BackendError { - match error { - RusotoError::Service(ListObjectsV2Error::NoSuchBucket(_)) => { - BackendError::RepositoryNotFound - } - _ => BackendError::S3Error(get_rusoto_error_message("ListObjectsV2", error)), - } - } -} - -impl From for BackendError { - fn from(error: DeError) -> BackendError { - BackendError::XmlParseError(format!("failed to parse xml: {error}")) - } -} -impl From for BackendError { - fn from(error: serde_xml_rs::Error) -> BackendError { - BackendError::XmlParseError(format!("failed to parse xml: {error}")) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use actix_web::body::to_bytes; - use actix_web::error::ResponseError; - use actix_web::http::StatusCode; - use bytes::Bytes; - use quick_xml::DeError; - use rusoto_core::RusotoError; - use rusoto_s3::{HeadObjectError, ListObjectsV2Error, PutObjectError}; - use serde_xml_rs::Error as XmlError; - - /// Tests for S3 error handling - mod s3_errors { - use super::*; - - #[tokio::test] - async fn should_convert_head_object_no_such_key_to_404() { - let error = RusotoError::Service(HeadObjectError::NoSuchKey("test-key".to_string())); - let backend_error = BackendError::from(error); - - assert!( - matches!(backend_error, BackendError::ObjectNotFound(_)), - "expected error to be ObjectNotFound" - ); - assert_eq!( - backend_error.status_code(), - StatusCode::NOT_FOUND, - "expected status code to be 404" - ); - let response = backend_error.error_response(); - assert_eq!(response.status(), StatusCode::NOT_FOUND); - assert_eq!( - to_bytes(response.into_body()).await.unwrap(), - Bytes::from("object not found: test-key") - ); - } - - #[tokio::test] - async fn should_convert_list_objects_no_such_bucket_to_404() { - let error = - RusotoError::Service(ListObjectsV2Error::NoSuchBucket("test-bucket".to_string())); - let backend_error = BackendError::from(error); - - assert!( - matches!(backend_error, BackendError::RepositoryNotFound), - "expected error to be converted to RepositoryNotFound" - ); - assert_eq!( - backend_error.status_code(), - StatusCode::NOT_FOUND, - "expected status code to be 404" - ); - let response = backend_error.error_response(); - assert_eq!(response.status(), StatusCode::NOT_FOUND); - assert_eq!( - to_bytes(response.into_body()).await.unwrap(), - Bytes::from("repository not found") - ); - } - - #[tokio::test] - async fn should_convert_put_object_unknown_error_to_502() { - let error: RusotoError = - RusotoError::Unknown(rusoto_core::request::BufferedHttpResponse { - status: StatusCode::INTERNAL_SERVER_ERROR, - headers: Default::default(), - body: Bytes::new(), - }); - let backend_error = BackendError::from(error); - - assert!( - matches!(backend_error, BackendError::S3Error(_)), - "expected error to be converted to S3Error" - ); - assert_eq!( - backend_error.status_code(), - StatusCode::BAD_GATEWAY, - "expected status code to be 502" - ); - let response = backend_error.error_response(); - assert_eq!(response.status(), StatusCode::BAD_GATEWAY); - assert_eq!( - to_bytes(response.into_body()).await.unwrap(), - Bytes::from("Internal Server Error: s3 error: PutObject Unknown Error: status 500 Internal Server Error") - ); - } - } - - /// Tests for Azure error handling - mod azure_errors { - use super::*; - - #[tokio::test] - async fn should_convert_not_found_to_404() { - let error = AzureError::new( - AzureErrorKind::HttpResponse { - status: AzureStatusCode::NotFound, - error_code: Some("ResourceNotFound".to_string()), - }, - "Resource not found", - ); - let backend_error = BackendError::from(error); - - assert!( - matches!(backend_error, BackendError::ObjectNotFound(_)), - "expected error to be converted to ObjectNotFound" - ); - assert_eq!( - backend_error.status_code(), - StatusCode::NOT_FOUND, - "expected status code to be 404" - ); - let response = backend_error.error_response(); - assert_eq!(response.status(), StatusCode::NOT_FOUND); - assert_eq!( - to_bytes(response.into_body()).await.unwrap(), - Bytes::from("object not found: ResourceNotFound") - ); - } - - #[tokio::test] - async fn should_convert_other_errors_to_502() { - let error = AzureError::new( - AzureErrorKind::HttpResponse { - status: AzureStatusCode::InternalServerError, - error_code: Some("InternalError".to_string()), - }, - "Internal error", - ); - let backend_error = BackendError::from(error); - - assert!( - matches!(backend_error, BackendError::AzureError(_)), - "expected error to be converted to AzureError" - ); - assert_eq!( - backend_error.status_code(), - StatusCode::BAD_GATEWAY, - "expected status code to be 502" - ); - let response = backend_error.error_response(); - assert_eq!(response.status(), StatusCode::BAD_GATEWAY); - assert_eq!( - to_bytes(response.into_body()).await.unwrap(), - Bytes::from("Internal Server Error: azure error: Internal error") - ); - } - } - - /// Tests for client-side error handling - mod client_errors { - use super::*; - - #[tokio::test] - async fn should_handle_unauthorized_error() { - let error = BackendError::UnauthorizedError; - assert_eq!( - error.status_code(), - StatusCode::UNAUTHORIZED, - "expected status code to be 401" - ); - assert_eq!( - error.to_string(), - "unauthorized", - "expected error message to be 'unauthorized'" - ); - let response = error.error_response(); - assert_eq!(response.status(), StatusCode::UNAUTHORIZED); - assert_eq!( - to_bytes(response.into_body()).await.unwrap(), - Bytes::from("unauthorized") - ); - } - - #[tokio::test] - async fn should_handle_invalid_request_error() { - let error = BackendError::InvalidRequest("bad input".to_string()); - assert_eq!( - error.status_code(), - StatusCode::BAD_REQUEST, - "expected status code to be 400" - ); - assert_eq!( - error.to_string(), - "invalid request", - "expected error message to be 'invalid request'" - ); - let response = error.error_response(); - assert_eq!(response.status(), StatusCode::BAD_REQUEST); - assert_eq!( - to_bytes(response.into_body()).await.unwrap(), - Bytes::from("invalid request") - ); - } - - #[tokio::test] - async fn should_handle_unsupported_auth_method() { - let error = BackendError::UnsupportedAuthMethod("basic".to_string()); - assert_eq!( - error.status_code(), - StatusCode::BAD_REQUEST, - "expected status code to be 400" - ); - assert_eq!( - error.to_string(), - "unsupported auth method: basic", - "expected error message to include auth method" - ); - let response = error.error_response(); - assert_eq!(response.status(), StatusCode::BAD_REQUEST); - assert_eq!( - to_bytes(response.into_body()).await.unwrap(), - Bytes::from("unsupported auth method: basic") - ); - } - - #[tokio::test] - async fn should_handle_unsupported_operation() { - let error = BackendError::UnsupportedOperation("delete".to_string()); - assert_eq!( - error.status_code(), - StatusCode::BAD_REQUEST, - "expected status code to be 400" - ); - assert_eq!( - error.to_string(), - "unsupported operation: delete", - "expected error message to include operation" - ); - let response = error.error_response(); - assert_eq!(response.status(), StatusCode::BAD_REQUEST); - assert_eq!( - to_bytes(response.into_body()).await.unwrap(), - Bytes::from("unsupported operation: delete") - ); - } - } - - /// Tests for XML parsing errors - mod xml_errors { - use super::*; - - #[test] - fn should_convert_quick_xml_error() { - let error = DeError::UnexpectedStart(b"unexpected start of stream".to_vec()); - let backend_error = BackendError::from(error); - - assert!( - matches!(backend_error, BackendError::XmlParseError(_)), - "expected error to be converted to XmlParseError" - ); - assert_eq!( - backend_error.status_code(), - StatusCode::INTERNAL_SERVER_ERROR, - "expected status code to be 500" - ); - } - - #[test] - fn should_convert_serde_xml_error() { - let error = XmlError::Custom { - field: "invalid XML format".to_string(), - }; - let backend_error = BackendError::from(error); - - assert!( - matches!(backend_error, BackendError::XmlParseError(_)), - "expected error to be converted to XmlParseError" - ); - assert_eq!( - backend_error.status_code(), - StatusCode::INTERNAL_SERVER_ERROR, - "expected status code to be 500" - ); - } - } - - /// Tests for API-related errors - mod api_errors { - use super::*; - - #[test] - fn should_handle_api_server_error() { - let error = BackendError::ApiServerError { - url: "https://api.example.com".to_string(), - status: 500, - message: "Internal Server Error".to_string(), - }; - assert_eq!( - error.status_code(), - StatusCode::BAD_GATEWAY, - "expected status code to be 502" - ); - assert!( - error.to_string().contains("api threw a server error"), - "expected error message to mention server error" - ); - } - - #[test] - fn should_handle_api_client_error_400() { - let error = BackendError::ApiClientError { - url: "https://api.example.com".to_string(), - status: 400, - message: "Bad Request".to_string(), - }; - assert_eq!( - error.status_code(), - StatusCode::BAD_REQUEST, - "expected status code to be 400" - ); - assert!( - error.to_string().contains("api threw a client error"), - "expected error message to mention client error" - ); - } - - #[test] - fn should_handle_api_client_error_404() { - let error = BackendError::ApiClientError { - url: "https://api.example.com".to_string(), - status: 404, - message: "Bad Request".to_string(), - }; - assert_eq!( - error.status_code(), - StatusCode::NOT_FOUND, - "expected status code to be 404" - ); - assert!( - error.to_string().contains("api threw a client error"), - "expected error message to mention client error" - ); - } - - #[test] - fn should_handle_json_parse_error() { - let error = BackendError::JsonParseError { - url: "https://api.example.com".to_string(), - }; - assert_eq!( - error.status_code(), - StatusCode::INTERNAL_SERVER_ERROR, - "expected status code to be 500" - ); - assert!( - error.to_string().contains("failed to parse JSON"), - "expected error message to mention JSON parsing" - ); - } - } - - /// Tests for repository-related errors - mod repository_errors { - use super::*; - - #[test] - fn should_handle_repository_not_found() { - let error = BackendError::RepositoryNotFound; - assert_eq!( - error.status_code(), - StatusCode::NOT_FOUND, - "expected status code to be 404" - ); - assert_eq!( - error.to_string(), - "repository not found", - "expected error message to be 'repository not found'" - ); - } - - #[test] - fn should_handle_repository_permissions_not_found() { - let error = BackendError::RepositoryPermissionsNotFound; - assert_eq!( - error.status_code(), - StatusCode::BAD_GATEWAY, - "expected status code to be 502" - ); - assert_eq!( - error.to_string(), - "failed to fetch repository permissions", - "expected error message to mention permissions" - ); - } - - #[test] - fn should_handle_source_repository_missing_primary_mirror() { - let error = BackendError::SourceRepositoryMissingPrimaryMirror; - assert_eq!( - error.status_code(), - StatusCode::NOT_FOUND, - "expected status code to be 404" - ); - assert_eq!( - error.to_string(), - "source repository missing primary mirror", - "expected error message to mention missing mirror" - ); - } - } - - /// Tests for data connection errors - mod data_connection_errors { - use super::*; - - #[test] - fn should_handle_data_connection_not_found() { - let error = BackendError::DataConnectionNotFound; - assert_eq!( - error.status_code(), - StatusCode::NOT_FOUND, - "expected status code to be 404" - ); - assert_eq!( - error.to_string(), - "data connection not found", - "expected error message to be 'data connection not found'" - ); - } - - #[test] - fn should_handle_unexpected_data_connection_provider() { - let error = BackendError::UnexpectedDataConnectionProvider { - provider: "unknown".to_string(), - }; - assert_eq!( - error.status_code(), - StatusCode::INTERNAL_SERVER_ERROR, - "expected status code to be 500" - ); - assert!( - error - .to_string() - .contains("unexpected data connection provider"), - "expected error message to mention unexpected provider" - ); - } - } -} diff --git a/src/utils/mod.rs b/src/utils/mod.rs deleted file mode 100644 index c80fecf..0000000 --- a/src/utils/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -pub mod api; -pub mod auth; -pub mod core; -pub mod errors; diff --git a/tests/pagination.rs b/tests/pagination.rs new file mode 100644 index 0000000..642280c --- /dev/null +++ b/tests/pagination.rs @@ -0,0 +1,113 @@ +#[path = "../src/pagination.rs"] +mod pagination; + +use multistore::api::list::parse_list_query_params; +use pagination::paginate_prefixes; + +// ── parse_list_query_params ──────────────────────────────────────── + +#[test] +fn parse_defaults_when_no_query() { + let params = parse_list_query_params(None); + assert_eq!(params.max_keys, 1000); + assert!(params.continuation_token.is_none()); + assert!(params.start_after.is_none()); +} + +#[test] +fn parse_max_keys() { + let params = parse_list_query_params(Some("list-type=2&max-keys=5")); + assert_eq!(params.max_keys, 5); +} + +#[test] +fn parse_max_keys_capped_at_1000() { + let params = parse_list_query_params(Some("max-keys=9999")); + assert_eq!(params.max_keys, 1000); +} + +#[test] +fn parse_continuation_token() { + let params = parse_list_query_params(Some("continuation-token=abc%2Fdef")); + assert_eq!(params.continuation_token.as_deref(), Some("abc/def")); +} + +#[test] +fn parse_start_after() { + let params = parse_list_query_params(Some("start-after=foo/")); + assert_eq!(params.start_after.as_deref(), Some("foo/")); +} + +// ── paginate_prefixes ────────────────────────────────────────────── + +fn prefixes(names: &[&str]) -> Vec { + names.iter().map(|s| s.to_string()).collect() +} + +#[test] +fn no_params_returns_all() { + let params = parse_list_query_params(None); + let result = paginate_prefixes(prefixes(&["c/", "a/", "b/"]), ¶ms); + assert_eq!(result.prefixes, vec!["a/", "b/", "c/"]); + assert!(!result.is_truncated); + assert!(result.next_continuation_token.is_none()); +} + +#[test] +fn max_keys_truncates() { + let params = parse_list_query_params(Some("max-keys=2")); + let result = paginate_prefixes(prefixes(&["e/", "d/", "c/", "b/", "a/"]), ¶ms); + assert_eq!(result.prefixes, vec!["a/", "b/"]); + assert!(result.is_truncated); + assert_eq!(result.next_continuation_token.as_deref(), Some("b/")); +} + +#[test] +fn continuation_token_skips_before() { + let params = parse_list_query_params(Some("continuation-token=b/")); + let result = paginate_prefixes(prefixes(&["a/", "b/", "c/", "d/"]), ¶ms); + assert_eq!(result.prefixes, vec!["c/", "d/"]); + assert!(!result.is_truncated); +} + +#[test] +fn start_after_skips_before() { + let params = parse_list_query_params(Some("start-after=b/")); + let result = paginate_prefixes(prefixes(&["a/", "b/", "c/", "d/"]), ¶ms); + assert_eq!(result.prefixes, vec!["c/", "d/"]); + assert!(!result.is_truncated); +} + +#[test] +fn continuation_token_takes_precedence_over_start_after() { + let params = parse_list_query_params(Some("start-after=a/&continuation-token=c/")); + let result = paginate_prefixes(prefixes(&["a/", "b/", "c/", "d/", "e/"]), ¶ms); + // continuation-token=c/ should win, skipping a/, b/, c/ + assert_eq!(result.prefixes, vec!["d/", "e/"]); +} + +#[test] +fn pagination_with_max_keys_and_continuation() { + let params = parse_list_query_params(Some("max-keys=2&continuation-token=b/")); + let result = paginate_prefixes(prefixes(&["a/", "b/", "c/", "d/", "e/"]), ¶ms); + assert_eq!(result.prefixes, vec!["c/", "d/"]); + assert!(result.is_truncated); + assert_eq!(result.next_continuation_token.as_deref(), Some("d/")); +} + +#[test] +fn empty_list_returns_empty() { + let params = parse_list_query_params(Some("max-keys=10")); + let result = paginate_prefixes(vec![], ¶ms); + assert!(result.prefixes.is_empty()); + assert!(!result.is_truncated); + assert!(result.next_continuation_token.is_none()); +} + +#[test] +fn max_keys_zero_returns_empty_but_truncated() { + let params = parse_list_query_params(Some("max-keys=0")); + let result = paginate_prefixes(prefixes(&["a/", "b/"]), ¶ms); + assert!(result.prefixes.is_empty()); + assert!(result.is_truncated); +} diff --git a/tests/routing.rs b/tests/routing.rs new file mode 100644 index 0000000..0653642 --- /dev/null +++ b/tests/routing.rs @@ -0,0 +1,188 @@ +// Include routing.rs directly so we can test on native targets without +// linking the full lib crate (which depends on wasm-only crates). +#[path = "../src/routing.rs"] +mod routing; + +use multistore_path_mapping::PathMapping; +use routing::{ + classify_request, extract_query_param, is_list_request, rewrite_prefix_in_query, RequestClass, +}; + +fn mapping() -> PathMapping { + PathMapping { + bucket_segments: 2, + bucket_separator: "--".to_string(), + display_bucket_segments: 1, + } +} + +// ── classify_request ──────────────────────────────────────────────── + +#[test] +fn root_path_returns_index() { + assert_eq!(classify_request(&mapping(), "/", None), RequestClass::Index); +} + +#[test] +fn empty_path_returns_index() { + assert_eq!(classify_request(&mapping(), "", None), RequestClass::Index); +} + +#[test] +fn object_request_two_segments_plus_key() { + assert_eq!( + classify_request(&mapping(), "/cholmes/admin-boundaries/file.parquet", None), + RequestClass::ProxyRequest { + rewritten_path: "/cholmes--admin-boundaries/file.parquet".to_string(), + query: None, + } + ); +} + +#[test] +fn object_request_nested_key() { + assert_eq!( + classify_request( + &mapping(), + "/cholmes/admin-boundaries/dir/sub/file.parquet", + None + ), + RequestClass::ProxyRequest { + rewritten_path: "/cholmes--admin-boundaries/dir/sub/file.parquet".to_string(), + query: None, + } + ); +} + +#[test] +fn product_list_via_path_segment() { + assert_eq!( + classify_request(&mapping(), "/cholmes/admin-boundaries", Some("list-type=2"),), + RequestClass::ProxyRequest { + rewritten_path: "/cholmes--admin-boundaries".to_string(), + query: Some("list-type=2".to_string()), + } + ); +} + +#[test] +fn account_list_no_prefix() { + assert_eq!( + classify_request(&mapping(), "/cholmes", Some("list-type=2")), + RequestClass::AccountList { + account: "cholmes".to_string(), + query: Some("list-type=2".to_string()), + } + ); +} + +#[test] +fn account_list_trailing_slash() { + assert_eq!( + classify_request(&mapping(), "/cholmes/", Some("list-type=2")), + RequestClass::AccountList { + account: "cholmes".to_string(), + query: Some("list-type=2".to_string()), + } + ); +} + +#[test] +fn prefix_routed_list() { + assert_eq!( + classify_request( + &mapping(), + "/cholmes", + Some("list-type=2&prefix=admin-boundaries/"), + ), + RequestClass::ProxyRequest { + rewritten_path: "/cholmes--admin-boundaries".to_string(), + query: Some("list-type=2&prefix=".to_string()), + } + ); +} + +#[test] +fn prefix_routed_list_with_subdir() { + assert_eq!( + classify_request( + &mapping(), + "/cholmes", + Some("list-type=2&prefix=admin-boundaries/subdir/"), + ), + RequestClass::ProxyRequest { + rewritten_path: "/cholmes--admin-boundaries".to_string(), + query: Some("list-type=2&prefix=subdir/".to_string()), + } + ); +} + +#[test] +fn single_segment_no_list_query() { + assert_eq!( + classify_request(&mapping(), "/cholmes", None), + RequestClass::BadRequest("Missing product in path".to_string()) + ); +} + +#[test] +fn url_encoded_prefix() { + assert_eq!( + classify_request( + &mapping(), + "/cholmes", + Some("list-type=2&prefix=admin%20boundaries/subdir/"), + ), + RequestClass::ProxyRequest { + rewritten_path: "/cholmes--admin boundaries".to_string(), + query: Some("list-type=2&prefix=subdir/".to_string()), + } + ); +} + +// ── Query helpers ─────────────────────────────────────────────────── + +#[test] +fn is_list_request_detects_list_type() { + assert!(is_list_request("list-type=2")); + assert!(is_list_request("foo=bar&list-type=2&baz=qux")); + assert!(!is_list_request("foo=bar")); + assert!(!is_list_request("")); +} + +#[test] +fn extract_query_param_finds_value() { + assert_eq!( + extract_query_param("list-type=2&prefix=foo/", "prefix"), + Some("foo/".to_string()) + ); +} + +#[test] +fn extract_query_param_missing() { + assert_eq!(extract_query_param("list-type=2", "prefix"), None); +} + +#[test] +fn extract_query_param_decodes_percent() { + assert_eq!( + extract_query_param("prefix=hello%20world", "prefix"), + Some("hello world".to_string()) + ); +} + +#[test] +fn rewrite_prefix_replaces_value() { + assert_eq!( + rewrite_prefix_in_query("list-type=2&prefix=old/", "new/"), + "list-type=2&prefix=new/" + ); +} + +#[test] +fn rewrite_prefix_to_empty() { + assert_eq!( + rewrite_prefix_in_query("prefix=old/&max-keys=100", ""), + "prefix=&max-keys=100" + ); +} diff --git a/tests/test_integration.py b/tests/test_integration.py new file mode 100644 index 0000000..71c6858 --- /dev/null +++ b/tests/test_integration.py @@ -0,0 +1,171 @@ +"""Integration tests for the Source Cooperative Data Proxy. + +Requires the worker to be running at the URL specified by PROXY_URL +(defaults to http://localhost:8787). +""" + +import os + +import requests + +PROXY_URL = os.environ.get("PROXY_URL", "http://localhost:8787") + +# Known public product for testing +ACCOUNT = "cholmes" +PRODUCT = "admin-boundaries" +OBJECT_KEY = "countries.parquet" + + +def test_index(): + resp = requests.get(f"{PROXY_URL}/") + assert resp.status_code == 200 + assert "Source Cooperative Data Proxy" in resp.text + + +def test_write_rejected(): + resp = requests.put(f"{PROXY_URL}/test/test/file.txt") + assert resp.status_code == 405 + + +def test_options_cors(): + resp = requests.options(f"{PROXY_URL}/", headers={"Origin": "https://example.com"}) + assert resp.status_code == 204 + assert resp.headers["access-control-allow-origin"] == "*" + assert "GET" in resp.headers["access-control-allow-methods"] + assert "HEAD" in resp.headers["access-control-allow-methods"] + + +def test_product_listing(): + resp = requests.get(f"{PROXY_URL}/{ACCOUNT}?list-type=2&delimiter=/") + assert resp.status_code == 200 + assert "CommonPrefixes" in resp.text + assert f"{PRODUCT}/" in resp.text + + +def test_file_listing(): + resp = requests.get( + f"{PROXY_URL}/{ACCOUNT}?list-type=2&prefix={PRODUCT}/&max-keys=5" + ) + assert resp.status_code == 200 + assert OBJECT_KEY in resp.text + + +def test_head_object(): + resp = requests.head(f"{PROXY_URL}/{ACCOUNT}/{PRODUCT}/{OBJECT_KEY}") + assert resp.status_code == 200 + assert "content-length" in resp.headers + assert int(resp.headers["content-length"]) > 0 + assert "etag" in resp.headers + assert "last-modified" in resp.headers + assert resp.headers.get("accept-ranges") == "bytes" + + +def test_get_object_range(): + resp = requests.get( + f"{PROXY_URL}/{ACCOUNT}/{PRODUCT}/{OBJECT_KEY}", + headers={"Range": "bytes=0-1023"}, + ) + assert resp.status_code in (200, 206) + assert len(resp.content) == 1024 + + +def test_head_object_range(): + resp = requests.head( + f"{PROXY_URL}/{ACCOUNT}/{PRODUCT}/{OBJECT_KEY}", + headers={"Range": "bytes=0-1023"}, + ) + assert resp.status_code in (200, 206) + content_length = int(resp.headers["content-length"]) + assert content_length == 1024 + + +def test_get_object_range_middle(): + """Request a range in the middle of the file.""" + resp = requests.get( + f"{PROXY_URL}/{ACCOUNT}/{PRODUCT}/{OBJECT_KEY}", + headers={"Range": "bytes=1000-1999"}, + ) + assert resp.status_code in (200, 206) + assert len(resp.content) == 1000 + + +def test_cors_on_get(): + resp = requests.get(f"{PROXY_URL}/") + assert resp.headers["access-control-allow-origin"] == "*" + assert resp.headers["access-control-expose-headers"] == "*" + + +def test_not_found(): + resp = requests.get( + f"{PROXY_URL}/{ACCOUNT}/{PRODUCT}/nonexistent-file-abc123.txt" + ) + assert resp.status_code == 404 + + +# --- Regression tests for bugs fixed during multistore refactor --- + + +def test_trailing_slash_equivalence(): + """Trailing slash on account should not break list requests.""" + resp = requests.get( + f"{PROXY_URL}/{ACCOUNT}/?list-type=2&prefix={PRODUCT}/&max-keys=5" + ) + assert resp.status_code == 200 + assert " in list response should be the account, not account--product.""" + resp = requests.get( + f"{PROXY_URL}/{ACCOUNT}?list-type=2&prefix={PRODUCT}/&max-keys=5" + ) + assert resp.status_code == 200 + assert f"{ACCOUNT}" in resp.text + assert "--" not in resp.text.split("")[1].split("")[0] + + +def test_xml_prefix_rewriting(): + """ should contain the original prefix, not the stripped one.""" + resp = requests.get( + f"{PROXY_URL}/{ACCOUNT}?list-type=2&prefix={PRODUCT}/&max-keys=5" + ) + assert resp.status_code == 200 + assert f"{PRODUCT}/" in resp.text + + +def test_key_prefixing(): + """ values in list responses should start with {product}/.""" + resp = requests.get( + f"{PROXY_URL}/{ACCOUNT}?list-type=2&prefix={PRODUCT}/&max-keys=5" + ) + assert resp.status_code == 200 + # Extract all values + import re + + keys = re.findall(r"(.*?)", resp.text) + assert len(keys) > 0, "Expected at least one key in response" + for key in keys: + assert key.startswith( + f"{PRODUCT}/" + ), f"Key '{key}' should start with '{PRODUCT}/'" + + +def test_object_access_via_path(): + """HEAD /{account}/{product}/{key} should return 200.""" + resp = requests.head(f"{PROXY_URL}/{ACCOUNT}/{PRODUCT}/{OBJECT_KEY}") + assert resp.status_code == 200 diff --git a/wrangler.preview.toml b/wrangler.preview.toml new file mode 100644 index 0000000..5d95033 --- /dev/null +++ b/wrangler.preview.toml @@ -0,0 +1,21 @@ +compatibility_date = "2026-03-17" +main = "build/worker/shim.mjs" +workers_dev = true + +[build] +command = "cargo install -q worker-build@0.7.5 && worker-build --release" + +[observability] +enabled = true +head_sampling_rate = 1 + +[observability.logs] +enabled = true +head_sampling_rate = 1 +invocation_logs = true +persist = true + +[observability.traces] +enabled = true +head_sampling_rate = 1 +persist = true diff --git a/wrangler.toml b/wrangler.toml new file mode 100644 index 0000000..ee06f47 --- /dev/null +++ b/wrangler.toml @@ -0,0 +1,44 @@ +compatibility_date = "2026-03-17" +main = "build/worker/shim.mjs" +name = "source-data-proxy" + +routes = [ + {pattern = "data.source.coop/*", zone_name = "coolnewgeo.com"}, + {pattern = "a.data.source.coop/*", zone_name = "coolnewgeo.com"}, + {pattern = "b.data.source.coop/*", zone_name = "coolnewgeo.com"}, + {pattern = "c.data.source.coop/*", zone_name = "coolnewgeo.com"}, + {pattern = "d.data.source.coop/*", zone_name = "coolnewgeo.com"}, +] + +[build] +command = "cargo install -q worker-build@0.7.5 && worker-build --release" + +[vars] +LOG_LEVEL = "WARN" +SOURCE_API_URL = "https://source.coop" + +# TODO: Set different sampling rates for prod vs staging +[observability] +enabled = true +head_sampling_rate = 1 + +[observability.logs] +enabled = true +head_sampling_rate = 1 +invocation_logs = true +persist = true + +[observability.traces] +enabled = true +head_sampling_rate = 1 +persist = true + +[env.staging] +routes = [ + {pattern = "data.staging.coolnewgeo.com/*", zone_name = "coolnewgeo.com"}, + {pattern = "data.staging.source.coop/*", zone_name = "coolnewgeo.com"}, +] + +[env.staging.vars] +LOG_LEVEL = "DEBUG" +SOURCE_API_URL = "https://staging.source.coop"