diff --git a/.cspell.json b/.cspell.json deleted file mode 100644 index 6cbcc82..0000000 --- a/.cspell.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "version": "0.1", - "language": "en", - "ignorePaths": [ - "**/node_modules/**", - "**/vscode-extension/**", - "**/.git/**", - ".vscode", - "package-lock.json", - "report" - ], - "words": [] -} diff --git a/.dockerfilelintrc b/.dockerfilelintrc deleted file mode 100644 index c097354..0000000 --- a/.dockerfilelintrc +++ /dev/null @@ -1,3 +0,0 @@ -rules: - missing_tag: off - invalid_port: off diff --git a/.dockerignore b/.dockerignore index 29d6828..b4a7fd4 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,3 +1,6 @@ node_modules npm-debug.log - +credentials.json +.git +.github +*.md diff --git a/.github/dependabot.yml b/.github/dependabot.yml index c5594f5..5a9b84b 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -8,12 +8,12 @@ updates: - package-ecosystem: "docker" directory: "/" # Location of package manifests schedule: - interval: "daily" + interval: "weekly" - package-ecosystem: "npm" directory: "/" # Location of package manifests schedule: - interval: "daily" + interval: "weekly" - package-ecosystem: "github-actions" directory: "/" # default location of `.github/workflows` schedule: - interval: "daily" + interval: "weekly" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..b75ad37 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,100 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +# Bucket used by the live tests and the container smoke test. +env: + BUCKET: s3proxy-public + IMAGE: s3proxy-docker:ci + +jobs: + lint: + name: Lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + - run: npm ci --no-audit --no-fund + - run: npm run lint + + test: + name: App Tests + runs-on: ubuntu-latest + needs: [lint] + # The app tests exercise live S3 (health, object GET, 404). Dependabot PRs + # don't receive repo secrets, so skip AWS-dependent jobs for them. + if: github.actor != 'dependabot[bot]' + steps: + - uses: actions/checkout@v4 + - uses: aws-actions/configure-aws-credentials@v4 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: ${{ secrets.AWS_REGION }} + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + - run: npm ci --no-audit --no-fund + - run: npm run test:app + + docker: + name: Docker Build & Smoke + runs-on: ubuntu-latest + needs: [lint] + if: github.actor != 'dependabot[bot]' + steps: + - uses: actions/checkout@v4 + - uses: docker/setup-buildx-action@v3 + - uses: aws-actions/configure-aws-credentials@v4 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: ${{ secrets.AWS_REGION }} + - uses: actions/setup-node@v4 + with: + node-version: 22 + - name: Build image + uses: docker/build-push-action@v6 + with: + context: . + target: production + load: true + tags: ${{ env.IMAGE }} + cache-from: type=gha + cache-to: type=gha,mode=max + # Structural checks (non-root user, layout, missing-BUCKET guard) run + # against the image just built, rather than rebuilding it. + - name: Container tests + run: npm run test:container + env: + S3PROXY_IMAGE: ${{ env.IMAGE }} + - name: Smoke test the container + run: | + docker run -d --name s3proxy -p 8080:8080 \ + -e BUCKET="$BUCKET" \ + -e AWS_ACCESS_KEY_ID -e AWS_SECRET_ACCESS_KEY -e AWS_SESSION_TOKEN \ + -e AWS_REGION "$IMAGE" + # Wait for the healthcheck to report healthy (max ~30s). + for i in $(seq 1 30); do + status=$(docker inspect -f '{{.State.Health.Status}}' s3proxy 2>/dev/null || echo starting) + echo "health: $status" + [ "$status" = "healthy" ] && break + sleep 2 + done + [ "$(docker inspect -f '{{.State.Health.Status}}' s3proxy)" = "healthy" ] + echo "--- GET /index.html ---" + curl -fsS -o /dev/null -w "status=%{http_code} type=%{content_type}\n" http://localhost:8080/index.html + echo "--- GET missing key (expect 404) ---" + code=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:8080/does-not-exist) + [ "$code" = "404" ] || { echo "expected 404, got $code"; exit 1; } + - name: Container logs + if: always() + run: docker logs s3proxy || true diff --git a/.github/workflows/continuous-deployment.yml b/.github/workflows/continuous-deployment.yml deleted file mode 100644 index e00f695..0000000 --- a/.github/workflows/continuous-deployment.yml +++ /dev/null @@ -1,133 +0,0 @@ ---- -# Mega-Linter GitHub Action configuration file -# More info at https://megalinter.github.io -name: Mega-Linter - -on: - # Trigger mega-linter at every push. Action will also be visible from Pull Requests to master - push: # Comment this line to trigger action only on pull-requests (not recommended if you don't pay for GH Actions) - branches: - - main - -env: # Comment env block if you do not want to apply fixes - # Apply linter fixes configuration - APPLY_FIXES: all # When active, APPLY_FIXES must also be defined as environment variable (in github/workflows/mega-linter.yml or other CI tool) - APPLY_FIXES_EVENT: pull_request # Decide which event triggers application of fixes in a commit or a PR (pull_request, push, all) - APPLY_FIXES_MODE: commit # If APPLY_FIXES is used, defines if the fixes are directly committed (commit) or posted in a PR (pull_request) - -jobs: - # Cancel duplicate jobs: https://github.com/fkirc/skip-duplicate-actions#option-3-cancellation-only - cancel_duplicates: - name: Cancel duplicate jobs - runs-on: ubuntu-latest - steps: - - uses: fkirc/skip-duplicate-actions@master - with: - github_token: ${{ secrets.PAT || secrets.GITHUB_TOKEN }} - cancel_others: true - - build: - name: Mega-Linter - runs-on: ubuntu-latest - steps: - # Git Checkout - - name: Checkout Code - uses: actions/checkout@v2 - with: - token: ${{ secrets.PAT || secrets.GITHUB_TOKEN }} - fetch-depth: 0 - # Mega-Linter - - name: Mega-Linter - id: ml - # You can override Mega-Linter flavor used to have faster performances - # More info at https://megalinter.github.io/flavors/ - uses: megalinter/megalinter/flavors/javascript@v5 - env: - # All available variables are described in documentation - # https://megalinter.github.io/configuration/ - VALIDATE_ALL_CODEBASE: true # Set ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }} to validate only diff with main branch - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - # ADD YOUR CUSTOM ENV VARIABLES HERE TO OVERRIDE VALUES OF .mega-linter.yml AT THE ROOT OF YOUR REPOSITORY - - # Upload Mega-Linter artifacts - - name: Archive production artifacts - if: ${{ success() }} || ${{ failure() }} - uses: actions/upload-artifact@v2 - with: - name: Mega-Linter reports - path: | - report - mega-linter.log - - # Create pull request if applicable (for now works only on PR from same repository, not from forks) - - name: Create Pull Request with applied fixes - id: cpr - if: steps.ml.outputs.has_updated_sources == 1 && (env.APPLY_FIXES_EVENT == 'all' || env.APPLY_FIXES_EVENT == github.event_name) && env.APPLY_FIXES_MODE == 'pull_request' && (github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository) && !contains(github.event.head_commit.message, 'skip fix') - uses: peter-evans/create-pull-request@v3 - with: - token: ${{ secrets.PAT || secrets.GITHUB_TOKEN }} - commit-message: "[Mega-Linter] Apply linters automatic fixes" - title: "[Mega-Linter] Apply linters automatic fixes" - labels: bot - - name: Create PR output - if: steps.ml.outputs.has_updated_sources == 1 && (env.APPLY_FIXES_EVENT == 'all' || env.APPLY_FIXES_EVENT == github.event_name) && env.APPLY_FIXES_MODE == 'pull_request' && (github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository) && !contains(github.event.head_commit.message, 'skip fix') - run: | - echo "Pull Request Number - ${{ steps.cpr.outputs.pull-request-number }}" - echo "Pull Request URL - ${{ steps.cpr.outputs.pull-request-url }}" - - # Push new commit if applicable (for now works only on PR from same repository, not from forks) - - name: Prepare commit - if: steps.ml.outputs.has_updated_sources == 1 && (env.APPLY_FIXES_EVENT == 'all' || env.APPLY_FIXES_EVENT == github.event_name) && env.APPLY_FIXES_MODE == 'commit' && github.ref != 'refs/heads/main' && (github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository) && !contains(github.event.head_commit.message, 'skip fix') - run: sudo chown -Rc $UID .git/ - - name: Commit and push applied linter fixes - if: steps.ml.outputs.has_updated_sources == 1 && (env.APPLY_FIXES_EVENT == 'all' || env.APPLY_FIXES_EVENT == github.event_name) && env.APPLY_FIXES_MODE == 'commit' && github.ref != 'refs/heads/main' && (github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository) && !contains(github.event.head_commit.message, 'skip fix') - uses: stefanzweifel/git-auto-commit-action@v4 - with: - branch: ${{ github.event.pull_request.head.ref || github.head_ref || github.ref }} - commit_message: "[Mega-Linter] Apply linters fixes" - test: - name: Unit-Test - runs-on: ubuntu-latest - env: - BUCKET: s3proxy-public - PORT: 8080 - AWS_REGION: us-east-1 - steps: - # Git Checkout - - name: Checkout Code - uses: actions/checkout@v2 - - name: Install Dependencies - run: npm ci --no-audit - - name: Test - run: npm test - - publish: - runs-on: ubuntu-latest - env: - DOCKER_CONTENT_TRUST: 0 - steps: - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v1 - - name: Set up QEMU - uses: docker/setup-qemu-action@v1 - - uses: actions/checkout@v2 - - name: Use Node.js - uses: actions/setup-node@v2 - with: - node-version: 16 - - name: Install syft - run: | - curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b /usr/local/bin - - name: docker build and push to dockerhub - env: - DOCKERHUB_USER: ${{ secrets.DOCKERHUB_USER }} - DOCKERHUB_ACCESS_TOKEN: ${{ secrets.DOCKERHUB_ACCESS_TOKEN }} - run: | - npm run docker-login-dockerhub - npm run dockerize-for-prod-dockerhub - - name: scan container - run: | - docker --version - docker scan s3proxy:test || exit 0 - - diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..ce3f40e --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,75 @@ +name: Publish + +# Publishes forkzero/s3proxy to Docker Hub when a GitHub Release is published. +# The release tag (e.g. v4.1.0) drives the image tags via docker/metadata-action. +on: + release: + types: [published] + workflow_dispatch: + +env: + IMAGE: forkzero/s3proxy + +jobs: + publish: + name: Build & Push + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@v4 + + - name: Docker metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.IMAGE }} + tags: | + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=raw,value=latest,enable={{is_default_branch}} + + - uses: docker/setup-qemu-action@v3 + - uses: docker/setup-buildx-action@v3 + + - name: Log in to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USER }} + password: ${{ secrets.DOCKERHUB_ACCESS_TOKEN }} + + - name: Build and push + uses: docker/build-push-action@v6 + with: + context: . + target: production + platforms: linux/amd64,linux/arm64 + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + build-args: | + VERSION=${{ steps.meta.outputs.version }} + cache-from: type=gha + cache-to: type=gha,mode=max + + scan: + name: Trivy Scan + runs-on: ubuntu-latest + needs: [publish] + permissions: + contents: read + security-events: write + steps: + - name: Scan published image + uses: aquasecurity/trivy-action@0.28.0 + with: + image-ref: ${{ env.IMAGE }}:latest + format: sarif + output: trivy-results.sarif + severity: CRITICAL,HIGH + exit-code: '0' # report only; don't fail the pipeline + - name: Upload results to code scanning + uses: github/codeql-action/upload-sarif@v3 + if: always() + with: + sarif_file: trivy-results.sarif diff --git a/.jscpd.json b/.jscpd.json deleted file mode 100644 index 040fb36..0000000 --- a/.jscpd.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "threshold": 0, - "reporters": ["html", "markdown"], - "ignore": [ - "**/node_modules/**", - "**/.git/**", - "**/.rbenv/**", - "**/.venv'/**", - "**/*cache*/**", - "**/.github/**", - "**/.idea/**", - "**/report/**", - "**/*.svg" - ] -} diff --git a/.mega-linter.yml b/.mega-linter.yml deleted file mode 100644 index 8a0ca4e..0000000 --- a/.mega-linter.yml +++ /dev/null @@ -1,12 +0,0 @@ -# Configuration file for Mega-Linter -# See all available variables at https://megalinter.github.io/configuration/ and in linters documentation - -APPLY_FIXES: all # all, none, or list of linter keys -DEFAULT_BRANCH: main # Usually master or main -# ENABLE: # If you use ENABLE variable, all other languages/formats/tooling-formats will be disabled by default -# ENABLE_LINTERS: # If you use ENABLE_LINTERS variable, all other linters will be disabled by default -DISABLE: - # - COPYPASTE # Uncomment to disable checks of abusive copy-pastes - - SPELL # Comment to enable checks of spelling mistakes -SHOW_ELAPSED_TIME: true -FILEIO_REPORTER: false diff --git a/Dockerfile b/Dockerfile index 3815ee3..a17b8ef 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,107 +1,68 @@ -# Multi-stage Dockerfile for s3proxy-docker with Fastify -# Optimized for security, performance, and minimal attack surface - -# Build arguments -ARG NODE_VERSION=22.13.0 -ARG ALPINE_VERSION=3.20 - -# Base stage with Node.js -FROM node:${NODE_VERSION}-alpine${ALPINE_VERSION} AS base - -# Install security updates and required packages -RUN apk update && \ - apk upgrade && \ - apk add --no-cache \ - tini \ - curl \ - ca-certificates && \ - rm -rf /var/cache/apk/* - -# Create non-root user -RUN addgroup -g 1001 -S nodejs && \ - adduser -S s3proxy -u 1001 -G nodejs - -# Set working directory -WORKDIR /app - -# Copy package files -COPY package*.json ./ - -# Dependencies stage -FROM base AS deps - -# Install production dependencies -RUN npm ci --only=production --no-audit --no-fund && \ - npm cache clean --force - -# Development dependencies stage -FROM base AS dev-deps - -# Install all dependencies for development/testing -RUN npm ci --no-audit --no-fund - -# Test stage -FROM dev-deps AS test - -# Copy source code -COPY --chown=s3proxy:nodejs . . - -# Run tests -RUN npm run test && \ - npm run lint - -# Production build stage -FROM base AS production - -# Copy production dependencies -COPY --from=deps --chown=s3proxy:nodejs /app/node_modules ./node_modules - -# Copy application code -COPY --chown=s3proxy:nodejs server.js ./ -COPY --chown=s3proxy:nodejs package.json ./ - -# Set environment variables -ENV NODE_ENV=production \ - PORT=8080 \ - LOG_LEVEL=info \ - NODE_OPTIONS="--enable-source-maps --max-old-space-size=512" \ +# syntax=docker/dockerfile:1 +# +# Docker runtime for s3proxy v4 (https://github.com/gmoon/s3proxy). +# Streams objects from an S3 bucket via a Hono web server (server.js). +# +# Build: docker build --build-arg VERSION=$npm_package_version -t forkzero/s3proxy . +# Test: docker build --target test -t s3proxy:test . && docker run --rm s3proxy:test +# Run: docker run -e BUCKET=my-bucket -p 8080:8080 forkzero/s3proxy + +# s3proxy requires Node >= 22.13. Pin by digest in CI via Dependabot updates. +ARG NODE_VERSION=22-alpine + +######################################################################## +# base: OS packages, production dependencies, and the app. Shared by all +# stages and, on its own, the runnable production image. +######################################################################## +FROM node:${NODE_VERSION} AS base + +ARG VERSION +WORKDIR /src + +# Runtime defaults. Override at `docker run` time with -e. +ENV PORT=8080 \ + NODE_ENV=production \ + DEBUG=s3proxy \ AWS_NODEJS_CONNECTION_REUSE_ENABLED=1 -# Expose port EXPOSE ${PORT} -# Health check -HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \ - CMD curl -f http://localhost:${PORT}/health || exit 1 - -# Switch to non-root user -USER s3proxy - -# Use tini as init system for proper signal handling -ENTRYPOINT ["/sbin/tini", "--"] - -# Start application -CMD ["node", "server.js"] - -# Development stage -FROM dev-deps AS development +# tini as PID 1 for correct signal handling. The healthcheck uses BusyBox +# wget (already in Alpine), so no extra package is needed for it. +RUN apk --no-cache --update-cache upgrade \ + && apk add --no-cache tini -# Copy source code -COPY --chown=s3proxy:nodejs . . +COPY package.json package-lock.json ./ +# BuildKit cache mount keeps the npm cache across builds without baking it +# into the layer, so no `npm cache clean` step is needed. +RUN --mount=type=cache,target=/root/.npm \ + npm ci --omit=dev --no-audit --no-fund -# Set development environment -ENV NODE_ENV=development \ - LOG_LEVEL=debug \ - PORT=8080 +COPY server.js ./ -# Expose port -EXPOSE ${PORT} +# Use 127.0.0.1 (not localhost): the server binds IPv4 0.0.0.0, while Alpine +# resolves "localhost" to IPv6 ::1 first, which would refuse the connection. +HEALTHCHECK --interval=30s --timeout=10s --start-period=15s --retries=3 \ + CMD wget -q -O /dev/null "http://127.0.0.1:${PORT}/health" || exit 1 -# Switch to non-root user -USER s3proxy - -# Use tini for signal handling +USER node ENTRYPOINT ["/sbin/tini", "--"] +CMD ["node", "server.js"] -# Start with hot reload -CMD ["npm", "run", "dev"] +######################################################################## +# test: adds dev dependencies and the full source, runs the unit tests. +######################################################################## +FROM base AS test + +USER root +ENV NODE_ENV=development +RUN --mount=type=cache,target=/root/.npm \ + npm ci --no-audit --no-fund +COPY . . +USER node +CMD ["npm", "test"] + +######################################################################## +# production: default build target. Inherits everything from base. +######################################################################## +FROM base AS production diff --git a/PROGRESS.md b/PROGRESS.md deleted file mode 100644 index ccb02b3..0000000 --- a/PROGRESS.md +++ /dev/null @@ -1,163 +0,0 @@ -# s3proxy-docker Migration Progress - -## ๐ŸŽฏ Project Goal -Migrate s3proxy-docker from Express to Fastify with modern tooling, security hardening, and comprehensive testing integration. - -## โœ… Completed Tasks - -### 1. Core Migration (100% Complete) -- **โœ… Replaced Express with Fastify** - Complete rewrite using Fastify 5.x -- **โœ… Modern server.js** - Clean implementation with no Express references -- **โœ… Performance improvements** - 2-3x faster than Express baseline -- **โœ… Built-in features** - JSON parsing, logging, error handling integrated -- **โœ… Security headers** - @fastify/helmet properly configured -- **โœ… Graceful shutdown** - Proper SIGTERM/SIGINT signal handling - -### 2. Dependencies & Tooling (100% Complete) -- **โœ… Updated package.json** - Modern dependencies, reduced from 948 to 194 packages (79% reduction) -- **โœ… s3proxy 3.0.0** - Latest library version integrated -- **โœ… Node.js 22.13.0+** - Aligned with main s3proxy requirements -- **โœ… ESM-only architecture** - Modern module system throughout -- **โœ… Biome linting** - Fast, modern code quality tool configured -- **โœ… All linting issues resolved** - Clean codebase with consistent formatting - -### 3. Docker Modernization (100% Complete) -- **โœ… Multi-stage Dockerfile** - Optimized build process with security hardening -- **โœ… Non-root user** - Security hardening (s3proxy:1001) -- **โœ… Alpine Linux base** - Minimal attack surface -- **โœ… Health checks** - Production-ready monitoring endpoints -- **โœ… Tini init system** - Proper signal handling in containers -- **โœ… Read-only filesystem** - Additional security layer -- **โœ… Docker Compose** - Development and production configurations - -### 4. Configuration Updates (100% Complete) -- **โœ… Correct bucket name** - Updated all references from `.test-bucket` to `s3proxy-public` -- **โœ… Environment variables** - Proper defaults and validation -- **โœ… Biome configuration** - Code quality and formatting rules -- **โœ… Makefile** - Build automation aligned with main s3proxy patterns - -### 5. Basic Testing (90% Complete) -- **โœ… Node.js built-in test runner** - No external test dependencies -- **โœ… Docker container tests** - Build validation, security checks, file structure -- **โœ… Basic functionality tests** - Health checks, version endpoints -- **โœ… Linting integration** - Automated code quality checks -- **โš ๏ธ Server integration tests** - Created but failing due to S3 initialization issues - -## ๐Ÿ”„ In Progress / Partially Complete - -### 1. Shared Testing Integration (50% Complete) -- **โœ… Shared testing framework created** - Basic structure implemented -- **โœ… Artillery integration planned** - Configuration files created -- **โŒ External dependency issue** - Cannot access ../s3proxy/shared-testing directory -- **โŒ S3 credential requirement** - Tests fail without AWS access - -### 2. AWS Credentials Integration (0% Complete) -- **โŒ Development credential handling** - Need to implement credentials.json support -- **โŒ Production credential chain** - AWS SDK credential chain needs testing -- **โŒ Test environment setup** - Need AWS credentials for realistic testing - -## ๐Ÿšง Decisions Still Needed - -### 1. Shared Testing Strategy -**Options to choose from:** -- **Option A**: Self-contained testing (copy essential configs into project) -- **Option B**: NPM package approach (@s3proxy/shared-testing module) -- **Option C**: Runtime download from GitHub -- **Option D**: Docker-specific testing only - -**Recommendation**: Option A (self-contained) for simplicity and no external dependencies - -### 2. AWS Credentials Testing Strategy -**Options to consider:** -- **Local development**: Use `aws sts get-session-token` โ†’ credentials.json -- **CI/CD integration**: Use GitHub Actions secrets or AWS IAM roles -- **Mock testing**: Create S3 mocks for basic functionality testing -- **Hybrid approach**: Basic tests without S3, integration tests with credentials - -### 3. Test Coverage Scope -**Decisions needed:** -- Which tests require real S3 bucket access? -- Should we test with actual s3proxy-public bucket or create test bucket? -- How to handle tests in environments without AWS access? - -## ๐Ÿ“‹ Remaining Tasks - -### High Priority -1. **๐Ÿ”ง Fix S3 Initialization for Testing** - - Implement proper credential handling for development - - Create test scenarios that work without S3 (health checks, version, 404s) - - Add conditional S3 tests that run only when credentials available - -2. **๐Ÿงช Complete Testing Integration** - - Decide on shared testing approach (recommend self-contained) - - Implement Docker-specific test scenarios - - Create performance testing with Artillery - - Add integration tests with real AWS credentials - -3. **๐Ÿ“š Documentation Updates** - - Update README.md with new Fastify-based setup - - Document AWS credential setup for development - - Add deployment guides for production environments - -### Medium Priority -4. **๐Ÿ”’ Security Enhancements** - - Add rate limiting (@fastify/rate-limit) - - Implement request logging for production - - Add security scanning to CI/CD pipeline - -5. **๐Ÿ“Š Monitoring & Observability** - - Add Prometheus metrics (@fastify/metrics) - - Implement structured logging for production - - Add performance monitoring endpoints - -6. **๐Ÿš€ CI/CD Pipeline** - - GitHub Actions workflow for automated testing - - Docker image publishing to registry - - Automated security scanning - -### Low Priority -7. **๐ŸŽฏ Advanced Features** - - Kubernetes deployment manifests - - Helm charts for K8s deployment - - OpenTelemetry integration - - Advanced caching strategies - -## โš ๏ธ Critical Blockers - -### 1. AWS Credentials for Testing -**Issue**: Tests fail because S3Proxy requires valid AWS credentials to initialize -**Impact**: Cannot run realistic integration tests -**Solutions needed**: -- Development credential setup documentation -- Mock S3 implementation for basic tests -- Conditional test execution based on credential availability - -### 2. Shared Testing Dependencies -**Issue**: Cannot access shared-testing directory outside project -**Impact**: Cannot leverage existing test scenarios and configurations -**Solutions needed**: -- Choose self-contained approach -- Copy essential test configurations into project -- Create Docker-specific test scenarios - -## ๐ŸŽฏ Next Steps (Recommended Order) - -1. **Implement AWS credential handling** for development environment -2. **Create self-contained test scenarios** for Docker container -3. **Fix server integration tests** to work with/without S3 credentials -4. **Add performance testing** with Artillery -5. **Update documentation** with setup and deployment guides -6. **Implement CI/CD pipeline** for automated testing and deployment - -## ๐Ÿ“Š Current Status Summary - -- **Core Migration**: โœ… 100% Complete -- **Docker Modernization**: โœ… 100% Complete -- **Basic Testing**: โš ๏ธ 90% Complete (blocked by AWS credentials) -- **Shared Testing**: โš ๏ธ 50% Complete (blocked by external dependencies) -- **Documentation**: โŒ 20% Complete -- **CI/CD**: โŒ 0% Complete - -**Overall Progress**: ~75% Complete - -The project has successfully migrated from Express to Fastify with significant improvements in performance, security, and maintainability. The main remaining work is around testing integration and AWS credential handling. diff --git a/README.md b/README.md index ba380a3..6073e83 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,101 @@ # s3proxy-docker -Docker runtime for S3Proxy NodeJS Express Application + +A small, production-ready Docker image that fronts an AWS S3 bucket with a web +server you control. It streams objects straight from S3 to the client โ€” no +local buffering โ€” using [s3proxy](https://github.com/gmoon/s3proxy) v4 and +[Hono](https://hono.dev) on `@hono/node-server`. + +Published image: **[`forkzero/s3proxy`](https://hub.docker.com/r/forkzero/s3proxy)** + +## Quick start + +```bash +docker run --rm -p 8080:8080 -e BUCKET=my-bucket forkzero/s3proxy +``` + +Then request any object by its key: + +```bash +curl http://localhost:8080/index.html +``` + +In production, credentials come from the standard AWS SDK credential chain +(IAM instance/task role, `AWS_*` environment variables, etc.) โ€” nothing to +configure beyond the bucket. + +## Configuration + +| Variable | Default | Description | +| ------------------------------------ | ------------ | ------------------------------------------------------ | +| `BUCKET` | *(required)* | S3 bucket to serve. | +| `PORT` | `8080` | Port the server listens on. | +| `AWS_REGION` | โ€“ | AWS region (per the AWS SDK). | +| `NODE_ENV` | `production` | `development` enables the dev credentials file (below). | +| `AWS_NODEJS_CONNECTION_REUSE_ENABLED`| `1` | Reuse HTTPS connections to S3 (set in the image). | + +## Endpoints + +| Route | Description | +| ----------------- | --------------------------------------------------------------------- | +| `GET \| HEAD /*` | Stream the object at that key from S3. | +| `GET /health` | Liveness + S3 connectivity check (drives the container `HEALTHCHECK`).| +| `GET /version` | Reports the s3proxy and Node versions. | +| `GET /` | Redirects to `/index.html`. | + +Missing or forbidden keys return an honest `404` / `403` with an XML error body +(no v3-style empty-`200`). + +## Local development + +To run against a bucket that requires credentials, mint a short-lived session +token and bind-mount it. The file is only read when `NODE_ENV` is **not** +`production`: + +```bash +npm run credentials # writes ./credentials.json via `aws sts get-session-token` +docker run --rm -p 8080:8080 \ + -e BUCKET=my-bucket -e NODE_ENV=development \ + -v "$PWD/credentials.json:/src/credentials.json:ro" \ + forkzero/s3proxy +``` + +Run the server directly (no Docker) the same way: + +```bash +BUCKET=my-bucket npm start # or: npm run dev (watch mode) +``` + +## Testing & build + +```bash +npm test # unit + container tests (needs AWS access to the bucket) +npm run lint # Biome +npm run docker:build # build the image locally +npm run docker:test # build the `test` stage and run the suite in-container +``` + +Or via the `Makefile`: `make build`, `make lint`, `make test`, `make docker-test`. + +## Image layout + +The [`Dockerfile`](./Dockerfile) is a multi-stage build: + +- **`base`** โ€“ Alpine + Node 22, production dependencies, and `server.js`. + Runs as the non-root `node` user with `tini` as PID 1 for clean signal + handling. This is the runnable image. +- **`test`** โ€“ adds dev dependencies and the full source, runs `npm test`. +- **`production`** โ€“ the default build target (inherits `base`). + +Build a specific stage with `--target`, e.g. `docker build --target test .`. + +## Releasing + +CI (`.github/workflows/ci.yml`) lints, runs the tests, and smoke-tests the +built image on every push/PR. Publishing `forkzero/s3proxy` to Docker Hub is +handled by `.github/workflows/publish.yml` when a GitHub Release is published; +the release tag drives the image tags (e.g. `4.1.0`, `4.1`, `latest`), and the +image is built for `linux/amd64` and `linux/arm64`. + +## License + +Apache-2.0 โ€” see [LICENSE](./LICENSE). diff --git a/S3PROXY_DOCKER_ADAPTATION_GUIDE.md b/S3PROXY_DOCKER_ADAPTATION_GUIDE.md deleted file mode 100644 index 741ef1c..0000000 --- a/S3PROXY_DOCKER_ADAPTATION_GUIDE.md +++ /dev/null @@ -1,513 +0,0 @@ -# S3Proxy-Docker Repository Adaptation Guide - -This guide provides step-by-step instructions for adapting the `forkzero/s3proxy-docker` repository to work with the new TypeScript-based s3proxy v3.0.0 and shared testing infrastructure. - -## Overview - -The s3proxy npm package now includes shared testing configurations that both the npm package and Docker container can use. This eliminates duplication and ensures consistent testing between deployment methods. - -## Required Changes - -### 1. Update Dockerfile for TypeScript/ESM Support - -Your Dockerfile needs to be updated to handle the new package structure: - -```dockerfile -# Multi-stage build for s3proxy v3.0.0 -FROM node:20-alpine AS base -WORKDIR /app - -# Install dependencies stage -FROM base AS deps -COPY package*.json ./ -RUN npm ci --only=production && npm cache clean --force - -# Application stage -FROM base AS app -COPY --from=deps /app/node_modules ./node_modules - -# Copy application files -COPY examples/express-basic.js ./ -COPY package.json ./ - -# Set environment variables -ENV NODE_ENV=production -ENV PORT=8080 - -# Health check -HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ - CMD curl -f http://localhost:${PORT}/health || exit 1 - -# Expose port -EXPOSE ${PORT} - -# Start application -CMD ["node", "examples/express-basic.js"] -``` - -**Key Changes:** -- Use Node.js 20+ for full ESM support -- The main entry point is now `examples/express-basic.js` (compiled from TypeScript) -- Health check endpoint is `/health` - -### 2. Update GitHub Actions Workflow - -Create or update `.github/workflows/test-and-build.yml`: - -```yaml -name: Test and Build Docker Container - -on: - push: - branches: [main] - pull_request: - branches: [main] - repository_dispatch: - types: [test-integration, release-container] - workflow_dispatch: - inputs: - s3proxy_version: - description: 'S3Proxy version to test' - required: false - default: 'latest' - test_type: - description: 'Type of load test to run' - required: false - default: 'basic' - type: choice - options: - - basic - - sustained - - spike - - range - -env: - S3PROXY_VERSION: ${{ github.event.inputs.s3proxy_version || github.event.client_payload.npm_version || 'latest' }} - TEST_TYPE: ${{ github.event.inputs.test_type || 'basic' }} - -jobs: - download-shared-testing: - runs-on: ubuntu-latest - outputs: - cache-key: ${{ steps.cache-key.outputs.key }} - steps: - - name: Generate cache key - id: cache-key - run: echo "key=shared-testing-${{ env.S3PROXY_VERSION }}-${{ hashFiles('**/package.json') }}" >> $GITHUB_OUTPUT - - - name: Cache shared testing configs - id: cache-shared-testing - uses: actions/cache@v3 - with: - path: shared-testing/ - key: ${{ steps.cache-key.outputs.key }} - - - name: Download shared testing configs - if: steps.cache-shared-testing.outputs.cache-hit != 'true' - run: | - if [ "$S3PROXY_VERSION" = "latest" ]; then - DOWNLOAD_URL="https://github.com/gmoon/s3proxy/archive/main.tar.gz" - else - DOWNLOAD_URL="https://github.com/gmoon/s3proxy/archive/v${S3PROXY_VERSION}.tar.gz" - fi - - echo "Downloading shared testing configs from: $DOWNLOAD_URL" - curl -L "$DOWNLOAD_URL" | tar -xz --strip=2 s3proxy-*/shared-testing - - # Verify download - ls -la shared-testing/ - ls -la shared-testing/configs/ - ls -la shared-testing/scenarios/ - - - name: Upload shared testing artifact - uses: actions/upload-artifact@v3 - with: - name: shared-testing-configs - path: shared-testing/ - retention-days: 1 - - build-and-test: - needs: download-shared-testing - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Download shared testing configs - uses: actions/download-artifact@v3 - with: - name: shared-testing-configs - path: ./ - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Build Docker image - run: | - docker build \ - --build-arg S3PROXY_VERSION=${{ env.S3PROXY_VERSION }} \ - --tag s3proxy-docker:test \ - . - - - name: Start container for testing - run: | - docker run -d \ - --name s3proxy-test \ - --publish 8080:8080 \ - --env BUCKET=s3proxy-public \ - --env AWS_REGION=us-east-1 \ - --env NODE_ENV=production \ - s3proxy-docker:test - - # Wait for container to be ready - echo "Waiting for container to start..." - timeout 60 bash -c 'until curl -f http://localhost:8080/health; do sleep 2; done' - echo "Container is ready!" - - - name: Install Artillery for load testing - run: | - npm install -g artillery@latest - artillery --version - - - name: Run load tests - run: | - echo "Running $TEST_TYPE load tests..." - - TEST_ENVIRONMENT=docker-container \ - artillery run \ - --config shared-testing/configs/docker-container.yml \ - shared-testing/scenarios/${TEST_TYPE}-load.yml - - - name: Run health and functionality tests - run: | - echo "Testing basic functionality..." - - # Test health endpoint - curl -f http://localhost:8080/health - - # Test basic file serving (if index.html exists in test bucket) - curl -f http://localhost:8080/index.html -o /dev/null - - # Test range requests - curl -f http://localhost:8080/large.bin \ - --header "Range: bytes=0-99" \ - --output /dev/null \ - --write-out "Status: %{http_code}, Content-Length: %{size_download}\n" - - # Test special characters (URL encoded) - SPECIAL_FILE=$(node -e "console.log(encodeURIComponent('specialCharacters!-_.*\\'()&\$@=;:+ ,?\\\\{^}%\`]\">[~<#|.'))") - curl -f "http://localhost:8080/$SPECIAL_FILE" -o /dev/null || echo "Special character test may have failed" - - - name: Collect container logs - if: always() - run: | - echo "=== Container Logs ===" - docker logs s3proxy-test - - echo "=== Container Stats ===" - docker stats s3proxy-test --no-stream - - - name: Upload test results - if: always() - uses: actions/upload-artifact@v3 - with: - name: test-results-${{ github.run_id }} - path: | - test-results-*.json - performance-comparison-*.json - retention-days: 7 - - - name: Cleanup - if: always() - run: | - docker stop s3proxy-test || true - docker rm s3proxy-test || true - - performance-comparison: - needs: [download-shared-testing, build-and-test] - runs-on: ubuntu-latest - if: github.event_name == 'repository_dispatch' && github.event.action == 'test-integration' - steps: - - name: Download shared testing configs - uses: actions/download-artifact@v3 - with: - name: shared-testing-configs - path: ./ - - - name: Download NPM package test results - run: | - # This would download results from the s3proxy repo - # Implementation depends on how you want to share results - echo "Downloading NPM package test results..." - # curl -L "${{ github.event.client_payload.npm_results_url }}" -o npm-results.json - - - name: Download Docker test results - uses: actions/download-artifact@v3 - with: - name: test-results-${{ github.run_id }} - path: ./ - - - name: Compare performance - run: | - node shared-testing/utils/results-parser.js compare \ - --docker-results test-results-docker-container-*.json \ - --npm-results npm-results.json - - - name: Report results back to s3proxy repo - if: always() - uses: peter-evans/repository-dispatch@v2 - with: - token: ${{ secrets.REPO_ACCESS_TOKEN }} - repository: gmoon/s3proxy - event-type: integration-result - client-payload: | - { - "success": "${{ job.status == 'success' }}", - "test_run_id": "${{ github.run_id }}", - "docker_repo_run_url": "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" - } - - build-and-push: - needs: build-and-test - runs-on: ubuntu-latest - if: github.ref == 'refs/heads/main' || github.event_name == 'repository_dispatch' - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Login to Docker Hub - uses: docker/login-action@v3 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - - name: Build and push Docker image - uses: docker/build-push-action@v5 - with: - context: . - platforms: linux/amd64,linux/arm64 - push: true - build-args: | - S3PROXY_VERSION=${{ env.S3PROXY_VERSION }} - tags: | - forkzero/s3proxy:${{ env.S3PROXY_VERSION }} - forkzero/s3proxy:latest - cache-from: type=gha - cache-to: type=gha,mode=max -``` - -### 3. Update Dockerfile Build Args - -Modify your Dockerfile to accept the s3proxy version as a build argument: - -```dockerfile -ARG S3PROXY_VERSION=latest - -FROM node:20-alpine AS base -WORKDIR /app - -FROM base AS deps -# Install specific version of s3proxy -ARG S3PROXY_VERSION -RUN if [ "$S3PROXY_VERSION" = "latest" ]; then \ - npm install s3proxy express; \ - else \ - npm install s3proxy@${S3PROXY_VERSION} express; \ - fi - -FROM base AS app -COPY --from=deps /app/node_modules ./node_modules - -# Create a simple Express server that uses s3proxy -COPY < { - try { - const stream = await proxy.healthCheckStream(res); - stream.on('error', () => res.end()).pipe(res); - } catch (error) { - res.status(500).end(); - } - }); - - // Serve all other requests from S3 - app.get('/*', async (req, res) => { - try { - const stream = await proxy.get(req, res); - stream.on('error', (err) => { - const statusCode = err.statusCode || 500; - res.status(statusCode).end(); - }).pipe(res); - } catch (error) { - res.status(500).end(); - } - }); - - app.listen(port, () => { - console.log(\`s3proxy-docker listening on port \${port}\`); - }); - } catch (error) { - console.error('Failed to start server:', error); - process.exit(1); - } -} - -startServer(); -EOF - -ENV NODE_ENV=production -ENV PORT=8080 - -HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ - CMD curl -f http://localhost:${PORT}/health || exit 1 - -EXPOSE ${PORT} -CMD ["node", "server.js"] -``` - -### 4. Update README.md - -Update your s3proxy-docker README to reflect the new version and testing approach: - -```markdown -# s3proxy-docker - -Official Docker container for [s3proxy](https://github.com/gmoon/s3proxy) - Stream files directly from AWS S3. - -## Quick Start - -```bash -docker run -p 8080:8080 -e BUCKET=your-bucket-name forkzero/s3proxy:3.0.0 -``` - -## Version 3.0 Changes - -- **TypeScript Support**: Built on s3proxy v3.0.0 with full TypeScript support -- **ESM Modules**: Modern ES module architecture -- **Improved Performance**: Enhanced streaming and error handling -- **Shared Testing**: Uses shared load testing infrastructure with npm package - -## Environment Variables - -- `BUCKET` - **Required**: S3 bucket name -- `PORT` - Server port (default: 8080) -- `AWS_REGION` - AWS region -- `NODE_ENV` - Environment (production/development) - -## Health Check - -The container includes a built-in health check endpoint at `/health` that verifies S3 connectivity. - -## Testing - -This container uses shared testing infrastructure with the s3proxy npm package to ensure consistent performance and functionality. - -### Load Testing - -```bash -# Download shared testing configs -curl -L https://github.com/gmoon/s3proxy/archive/main.tar.gz | \ - tar -xz --strip=2 s3proxy-main/shared-testing - -# Run load tests -artillery run --config shared-testing/configs/docker-container.yml \ - shared-testing/scenarios/basic-load.yml -``` - -## Supported Architectures - -- `linux/amd64` -- `linux/arm64` - -## Tags - -- `forkzero/s3proxy:3.0.0` - Specific version -- `forkzero/s3proxy:latest` - Latest stable version -``` - -### 5. Repository Secrets - -Add these secrets to your s3proxy-docker repository: - -- `DOCKERHUB_USERNAME` - Your Docker Hub username -- `DOCKERHUB_TOKEN` - Docker Hub access token -- `REPO_ACCESS_TOKEN` - GitHub token with repo access (for cross-repo communication) - -### 6. Testing the Integration - -After implementing these changes: - -1. **Test locally**: - ```bash - # Build the container - docker build --build-arg S3PROXY_VERSION=3.0.0 -t s3proxy-test . - - # Run the container - docker run -d -p 8080:8080 -e BUCKET=s3proxy-public s3proxy-test - - # Test health endpoint - curl http://localhost:8080/health - ``` - -2. **Test with shared configs**: - ```bash - # Download shared testing - curl -L https://github.com/gmoon/s3proxy/archive/main.tar.gz | \ - tar -xz --strip=2 s3proxy-main/shared-testing - - # Run load test - artillery run --config shared-testing/configs/docker-container.yml \ - shared-testing/scenarios/basic-load.yml - ``` - -## Integration Workflow - -The integration between repositories works as follows: - -1. **s3proxy repo** builds and tests npm package -2. **s3proxy repo** triggers s3proxy-docker integration test -3. **s3proxy-docker** downloads shared testing configs -4. **s3proxy-docker** builds container with new package version -5. **s3proxy-docker** runs load tests using shared configs -6. **s3proxy-docker** reports results back to s3proxy repo -7. If tests pass, both repos can release their respective artifacts - -This ensures that the Docker container performs equivalently to the npm package before any releases are made. - -## Troubleshooting - -### Common Issues - -1. **Container fails to start**: Check that `BUCKET` environment variable is set -2. **Health check fails**: Verify AWS credentials and bucket permissions -3. **Load tests fail**: Ensure test data exists in S3 bucket (run `setup-s3-data.sh`) - -### Debug Mode - -```bash -docker run -p 8080:8080 -e BUCKET=your-bucket -e NODE_ENV=development forkzero/s3proxy:3.0.0 -``` - -This guide should give you everything needed to adapt the s3proxy-docker repository to work with the new TypeScript-based s3proxy v3.0.0 and shared testing infrastructure. diff --git a/TODO.md b/TODO.md deleted file mode 100644 index 945a03d..0000000 --- a/TODO.md +++ /dev/null @@ -1,194 +0,0 @@ -# S3Proxy-Docker v3.0.0 Adaptation TODO - -This TODO list outlines the steps needed to adapt the s3proxy-docker repository to work with the new TypeScript-based s3proxy v3.0.0 and shared testing infrastructure. - -## Phase 1: Core Application Updates - -### 1.1 Update Dependencies -- [ ] Update `package.json` to use s3proxy v3.0.0 -- [ ] Update Node.js version requirement to 20+ for full ESM support -- [ ] Review and update other dependencies for compatibility -- [ ] Test dependency compatibility locally - -### 1.2 Application Code Migration -- [ ] **CRITICAL**: Replace `express-s3proxy.js` with new ESM-compatible version -- [ ] Update import statements from `require()` to ES modules -- [ ] Adapt to new s3proxy v3.0.0 API changes -- [ ] Update error handling for new TypeScript interfaces -- [ ] Test credential management with new version -- [ ] Verify AWS X-Ray integration still works - -### 1.3 Configuration Updates -- [ ] Update environment variable handling for new version -- [ ] Review and update default configurations -- [ ] Test health check endpoints with new API - -## Phase 2: Docker Infrastructure - -### 2.1 Dockerfile Modernization -- [ ] Update base image to Node.js 20-alpine -- [ ] Add build argument for S3PROXY_VERSION -- [ ] Update multi-stage build for TypeScript/ESM support -- [ ] Modify COPY commands for new file structure -- [ ] Update health check configuration -- [ ] Test Docker build locally - -### 2.2 Docker Scripts and Commands -- [ ] Update npm scripts in package.json for new Docker workflow -- [ ] Modify `dockerize-for-prod-aws` script -- [ ] Modify `dockerize-for-prod-dockerhub` script -- [ ] Update `docker` test script -- [ ] Test all Docker-related npm scripts - -## Phase 3: Testing Infrastructure - -### 3.1 Shared Testing Integration -- [ ] Remove duplicate test configurations -- [ ] Create mechanism to download shared testing configs from s3proxy repo -- [ ] Update test scripts to use shared configurations -- [ ] Integrate Artillery load testing with shared scenarios -- [ ] Test shared testing workflow locally - -### 3.2 Test Updates -- [ ] Update `test/test.js` for new s3proxy API -- [ ] Add tests for new TypeScript interfaces -- [ ] Update nock mocking for API changes -- [ ] Add integration tests with shared configs -- [ ] Verify all tests pass with new version - -## Phase 4: CI/CD Pipeline - -### 4.1 GitHub Actions Overhaul -- [ ] **MAJOR**: Replace `continuous-deployment.yml` with new workflow -- [ ] Add job for downloading shared testing configs -- [ ] Implement cross-repo integration testing -- [ ] Add performance comparison between npm and Docker -- [ ] Configure artifact sharing between jobs -- [ ] Add multi-platform build support - -### 4.2 Repository Secrets -- [ ] Verify `DOCKERHUB_USERNAME` secret exists -- [ ] Verify `DOCKERHUB_TOKEN` secret exists -- [ ] Add `REPO_ACCESS_TOKEN` for cross-repo communication -- [ ] Test secret access in workflows - -### 4.3 Integration Workflow -- [ ] Set up repository dispatch triggers -- [ ] Configure result reporting back to s3proxy repo -- [ ] Test end-to-end integration workflow -- [ ] Verify performance comparison functionality - -## Phase 5: Documentation - -### 5.1 README Updates -- [ ] Update README.md for v3.0.0 changes -- [ ] Document new environment variables -- [ ] Update usage examples -- [ ] Add shared testing documentation -- [ ] Update supported architectures info -- [ ] Add troubleshooting section for new version - -### 5.2 Additional Documentation -- [ ] Review and update inline code comments -- [ ] Update JSDoc comments for new API -- [ ] Create migration guide for existing users -- [ ] Document breaking changes - -## Phase 6: Quality Assurance - -### 6.1 Local Testing -- [ ] Test Docker build with new s3proxy version -- [ ] Verify container starts and serves files correctly -- [ ] Test health endpoints -- [ ] Test range requests -- [ ] Test special character handling -- [ ] Test error scenarios - -### 6.2 Load Testing -- [ ] Run basic load tests with shared configs -- [ ] Run sustained load tests -- [ ] Compare performance with previous version -- [ ] Verify memory usage and stability -- [ ] Test under various load patterns - -### 6.3 Integration Testing -- [ ] Test with real S3 buckets -- [ ] Test AWS credential handling -- [ ] Test in different AWS regions -- [ ] Test with various file types and sizes -- [ ] Test cross-platform compatibility (AMD64/ARM64) - -## Phase 7: Deployment Preparation - -### 7.1 Version Management -- [ ] Update version in package.json to match s3proxy version -- [ ] Create version tags strategy -- [ ] Plan backward compatibility approach -- [ ] Document version migration path - -### 7.2 Release Preparation -- [ ] Create release notes for v3.0.0 -- [ ] Prepare Docker Hub description updates -- [ ] Plan rollout strategy -- [ ] Prepare rollback plan if needed - -## Phase 8: Post-Migration - -### 8.1 Monitoring and Validation -- [ ] Monitor container performance in production -- [ ] Validate shared testing integration works -- [ ] Monitor for any regression issues -- [ ] Collect user feedback - -### 8.2 Cleanup -- [ ] Remove old/unused files -- [ ] Clean up deprecated npm scripts -- [ ] Archive old documentation -- [ ] Update project metadata - -## Critical Dependencies - -### External Requirements -- [ ] Ensure s3proxy v3.0.0 is published and stable -- [ ] Verify shared testing configs are available in s3proxy repo -- [ ] Confirm cross-repo integration approach with s3proxy maintainer - -### Risk Mitigation -- [ ] Create backup of current working version -- [ ] Test migration in separate branch -- [ ] Plan for gradual rollout -- [ ] Prepare communication for breaking changes - -## Priority Levels - -**HIGH PRIORITY** (Must complete for basic functionality): -- Update dependencies and application code -- Update Dockerfile for Node.js 20+ and ESM -- Basic testing with new version - -**MEDIUM PRIORITY** (Important for full integration): -- Shared testing infrastructure -- Updated CI/CD pipeline -- Documentation updates - -**LOW PRIORITY** (Nice to have): -- Performance optimizations -- Advanced monitoring -- Additional test scenarios - -## Estimated Timeline - -- **Phase 1-2**: 2-3 days (Core updates and Docker) -- **Phase 3-4**: 3-4 days (Testing and CI/CD) -- **Phase 5-6**: 2-3 days (Documentation and QA) -- **Phase 7-8**: 1-2 days (Deployment and cleanup) - -**Total Estimated Time**: 8-12 days - -## Notes - -- Keep the current version working until migration is complete -- Test each phase thoroughly before moving to the next -- Consider creating a migration branch for development -- Coordinate with s3proxy repo maintainer for integration testing -- Document any issues or deviations from the plan diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index 258e490..0000000 --- a/docker-compose.yml +++ /dev/null @@ -1,43 +0,0 @@ -version: '3.8' - -services: - s3proxy: - build: - context: . - dockerfile: Dockerfile - target: production - ports: - - "8080:8080" - environment: - - BUCKET=${BUCKET:-s3proxy-public} - - NODE_ENV=production - - LOG_LEVEL=info - healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:8080/health"] - interval: 30s - timeout: 10s - retries: 3 - start_period: 30s - restart: unless-stopped - security_opt: - - no-new-privileges:true - read_only: true - tmpfs: - - /tmp:noexec,nosuid,size=100m - - s3proxy-dev: - build: - context: . - dockerfile: Dockerfile - target: development - ports: - - "8080:8080" - environment: - - BUCKET=${BUCKET:-s3proxy-public} - - NODE_ENV=development - - LOG_LEVEL=debug - volumes: - - .:/app - - /app/node_modules - profiles: - - dev diff --git a/package-lock.json b/package-lock.json index 359ef7a..8afcced 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,22 +1,20 @@ { "name": "s3proxy-docker", - "version": "3.0.0", + "version": "4.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "s3proxy-docker", - "version": "3.0.0", + "version": "4.1.0", "license": "Apache-2.0", "dependencies": { - "@fastify/helmet": "^12.0.1", - "@fastify/sensible": "^6.0.1", - "fastify": "^5.4.0", - "s3proxy": "^3.0.0" + "@hono/node-server": "^2.0.0", + "hono": "^4.12.15", + "s3proxy": "^4.1.0" }, "devDependencies": { - "@biomejs/biome": "^1.9.4", - "pino-pretty": "^13.0.0" + "@biomejs/biome": "^1.9.4" }, "engines": { "node": ">=22.13.0" @@ -1056,157 +1054,16 @@ "node": ">=14.21.3" } }, - "node_modules/@fastify/ajv-compiler": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@fastify/ajv-compiler/-/ajv-compiler-4.0.2.tgz", - "integrity": "sha512-Rkiu/8wIjpsf46Rr+Fitd3HRP+VsxUFDDeag0hs9L0ksfnwx2g7SPQQTFL0E8Qv+rfXzQOxBJnjUB9ITUDjfWQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "MIT", - "dependencies": { - "ajv": "^8.12.0", - "ajv-formats": "^3.0.1", - "fast-uri": "^3.0.0" - } - }, - "node_modules/@fastify/error": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@fastify/error/-/error-4.2.0.tgz", - "integrity": "sha512-RSo3sVDXfHskiBZKBPRgnQTtIqpi/7zhJOEmAxCiBcM7d0uwdGdxLlsCaLzGs8v8NnxIRlfG0N51p5yFaOentQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "MIT" - }, - "node_modules/@fastify/fast-json-stringify-compiler": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/@fastify/fast-json-stringify-compiler/-/fast-json-stringify-compiler-5.0.3.tgz", - "integrity": "sha512-uik7yYHkLr6fxd8hJSZ8c+xF4WafPK+XzneQDPU+D10r5X19GW8lJcom2YijX2+qtFF1ENJlHXKFM9ouXNJYgQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "MIT", - "dependencies": { - "fast-json-stringify": "^6.0.0" - } - }, - "node_modules/@fastify/forwarded": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@fastify/forwarded/-/forwarded-3.0.0.tgz", - "integrity": "sha512-kJExsp4JCms7ipzg7SJ3y8DwmePaELHxKYtg+tZow+k0znUTf3cb+npgyqm8+ATZOdmfgfydIebPDWM172wfyA==", - "license": "MIT" - }, - "node_modules/@fastify/helmet": { - "version": "12.0.1", - "resolved": "https://registry.npmjs.org/@fastify/helmet/-/helmet-12.0.1.tgz", - "integrity": "sha512-kkjBcedWwdflRThovGuvN9jB2QQLytBqArCFPdMIb7o2Fp0l/H3xxYi/6x/SSRuH/FFt9qpTGIfJz2bfnMrLqA==", - "license": "MIT", - "dependencies": { - "fastify-plugin": "^5.0.0", - "helmet": "^7.1.0" - } - }, - "node_modules/@fastify/helmet/node_modules/helmet": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/helmet/-/helmet-7.2.0.tgz", - "integrity": "sha512-ZRiwvN089JfMXokizgqEPXsl2Guk094yExfoDXR0cBYWxtBbaSww/w+vT4WEJsBW2iTUi1GgZ6swmoug3Oy4Xw==", - "license": "MIT", - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@fastify/merge-json-schemas": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/@fastify/merge-json-schemas/-/merge-json-schemas-0.2.1.tgz", - "integrity": "sha512-OA3KGBCy6KtIvLf8DINC5880o5iBlDX4SxzLQS8HorJAbqluzLRn80UXU0bxZn7UOFhFgpRJDasfwn9nG4FG4A==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "MIT", - "dependencies": { - "dequal": "^2.0.3" - } - }, - "node_modules/@fastify/proxy-addr": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@fastify/proxy-addr/-/proxy-addr-5.0.0.tgz", - "integrity": "sha512-37qVVA1qZ5sgH7KpHkkC4z9SK6StIsIcOmpjvMPXNb3vx2GQxhZocogVYbr2PbbeLCQxYIPDok307xEvRZOzGA==", - "license": "MIT", - "dependencies": { - "@fastify/forwarded": "^3.0.0", - "ipaddr.js": "^2.1.0" - } - }, - "node_modules/@fastify/proxy-addr/node_modules/ipaddr.js": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz", - "integrity": "sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==", + "node_modules/@hono/node-server": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-2.0.8.tgz", + "integrity": "sha512-GuCWzLxwg218fy1JaHculFsdcuY12hxit83V+algozTPnwhNjLrRL/Alg9OYjLZLoUZ1rw/S4CdTMsnkSKCmFA==", "license": "MIT", "engines": { - "node": ">= 10" - } - }, - "node_modules/@fastify/sensible": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/@fastify/sensible/-/sensible-6.0.3.tgz", - "integrity": "sha512-Iyn8698hp/e5+v8SNBBruTa7UfrMEP52R16dc9jMpqSyEcPsvWFQo+R6WwHCUnJiLIsuci2ZoEZ7ilrSSCPIVg==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "MIT", - "dependencies": { - "@lukeed/ms": "^2.0.2", - "dequal": "^2.0.3", - "fastify-plugin": "^5.0.0", - "forwarded": "^0.2.0", - "http-errors": "^2.0.0", - "type-is": "^1.6.18", - "vary": "^1.1.2" - } - }, - "node_modules/@lukeed/ms": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@lukeed/ms/-/ms-2.0.2.tgz", - "integrity": "sha512-9I2Zn6+NJLfaGoz9jN3lpwDgAYvfGeNYdbAIjJOqzs4Tpc+VU3Jqq4IofSUBKajiDS8k9fZIg18/z13mpk1bsA==", - "license": "MIT", - "engines": { - "node": ">=8" + "node": ">=20" + }, + "peerDependencies": { + "hono": "^4" } }, "node_modules/@smithy/abort-controller": { @@ -1959,189 +1816,12 @@ "integrity": "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==", "license": "MIT" }, - "node_modules/abstract-logging": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz", - "integrity": "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==", - "license": "MIT" - }, - "node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", - "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/ajv-formats": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", - "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", - "license": "MIT", - "dependencies": { - "ajv": "^8.0.0" - }, - "peerDependencies": { - "ajv": "^8.0.0" - }, - "peerDependenciesMeta": { - "ajv": { - "optional": true - } - } - }, - "node_modules/atomic-sleep": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", - "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", - "license": "MIT", - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/avvio": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/avvio/-/avvio-9.1.0.tgz", - "integrity": "sha512-fYASnYi600CsH/j9EQov7lECAniYiBFiiAtBNuZYLA2leLe9qOvZzqYHFjtIj6gD2VMoMLP14834LFWvr4IfDw==", - "license": "MIT", - "dependencies": { - "@fastify/error": "^4.0.0", - "fastq": "^1.17.1" - } - }, "node_modules/bowser": { "version": "2.11.0", "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.11.0.tgz", "integrity": "sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA==", "license": "MIT" }, - "node_modules/colorette": { - "version": "2.0.20", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", - "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", - "dev": true, - "license": "MIT" - }, - "node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/dequal": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", - "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/end-of-stream": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", - "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", - "dev": true, - "license": "MIT", - "dependencies": { - "once": "^1.4.0" - } - }, - "node_modules/fast-copy": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/fast-copy/-/fast-copy-3.0.2.tgz", - "integrity": "sha512-dl0O9Vhju8IrcLndv2eU4ldt1ftXMqqfgN4H1cpmGV7P6jeB9FwpN9a2c8DPGE1Ys88rNUJVYDHq73CGAGOPfQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-decode-uri-component": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz", - "integrity": "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==", - "license": "MIT" - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "license": "MIT" - }, - "node_modules/fast-json-stringify": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/fast-json-stringify/-/fast-json-stringify-6.0.1.tgz", - "integrity": "sha512-s7SJE83QKBZwg54dIbD5rCtzOBVD43V1ReWXXYqBgwCwHLYAAT0RQc/FmrQglXqWPpz6omtryJQOau5jI4Nrvg==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "MIT", - "dependencies": { - "@fastify/merge-json-schemas": "^0.2.0", - "ajv": "^8.12.0", - "ajv-formats": "^3.0.1", - "fast-uri": "^3.0.0", - "json-schema-ref-resolver": "^2.0.0", - "rfdc": "^1.2.0" - } - }, - "node_modules/fast-querystring": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/fast-querystring/-/fast-querystring-1.1.2.tgz", - "integrity": "sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==", - "license": "MIT", - "dependencies": { - "fast-decode-uri-component": "^1.0.1" - } - }, - "node_modules/fast-redact": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/fast-redact/-/fast-redact-3.5.0.tgz", - "integrity": "sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/fast-safe-stringify": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", - "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-uri": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz", - "integrity": "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "BSD-3-Clause" - }, "node_modules/fast-xml-parser": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.4.1.tgz", @@ -2164,418 +1844,19 @@ "fxparser": "src/cli/cli.js" } }, - "node_modules/fastify": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/fastify/-/fastify-5.4.0.tgz", - "integrity": "sha512-I4dVlUe+WNQAhKSyv15w+dwUh2EPiEl4X2lGYMmNSgF83WzTMAPKGdWEv5tPsCQOb+SOZwz8Vlta2vF+OeDgRw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "MIT", - "dependencies": { - "@fastify/ajv-compiler": "^4.0.0", - "@fastify/error": "^4.0.0", - "@fastify/fast-json-stringify-compiler": "^5.0.0", - "@fastify/proxy-addr": "^5.0.0", - "abstract-logging": "^2.0.1", - "avvio": "^9.0.0", - "fast-json-stringify": "^6.0.0", - "find-my-way": "^9.0.0", - "light-my-request": "^6.0.0", - "pino": "^9.0.0", - "process-warning": "^5.0.0", - "rfdc": "^1.3.1", - "secure-json-parse": "^4.0.0", - "semver": "^7.6.0", - "toad-cache": "^3.7.0" - } - }, - "node_modules/fastify-plugin": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-5.0.1.tgz", - "integrity": "sha512-HCxs+YnRaWzCl+cWRYFnHmeRFyR5GVnJTAaCJQiYzQSDwK9MgJdyAsuL3nh0EWRCYMgQ5MeziymvmAhUHYHDUQ==", - "license": "MIT" - }, - "node_modules/fastq": { - "version": "1.18.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.18.0.tgz", - "integrity": "sha512-QKHXPW0hD8g4UET03SdOdunzSouc9N4AuHdsX8XNcTsuz+yYFILVNIX4l9yHABMhiEI9Db0JTTIpu0wB+Y1QQw==", - "license": "ISC", - "dependencies": { - "reusify": "^1.0.4" - } - }, - "node_modules/find-my-way": { - "version": "9.3.0", - "resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-9.3.0.tgz", - "integrity": "sha512-eRoFWQw+Yv2tuYlK2pjFS2jGXSxSppAs3hSQjfxVKxM5amECzIgYYc1FEI8ZmhSh/Ig+FrKEz43NLRKJjYCZVg==", + "node_modules/hono": { + "version": "4.12.30", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.30.tgz", + "integrity": "sha512-emn+JoJjrN9YTpRDS5it/UI2SO9BAE37T6I3d963RxcZ81G9A4pr2SZTEiiaiKbzx+NKRg5BZ89fCL7gCJCUog==", "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-querystring": "^1.0.0", - "safe-regex2": "^5.0.0" - }, "engines": { - "node": ">=20" + "node": ">=16.9.0" } }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/help-me": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/help-me/-/help-me-5.0.0.tgz", - "integrity": "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==", - "dev": true, - "license": "MIT" - }, - "node_modules/http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", - "license": "MIT", - "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "license": "ISC" - }, - "node_modules/joycon": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", - "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/json-schema-ref-resolver": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/json-schema-ref-resolver/-/json-schema-ref-resolver-2.0.1.tgz", - "integrity": "sha512-HG0SIB9X4J8bwbxCbnd5FfPEbcXAJYTi1pBJeP/QPON+w8ovSME8iRG+ElHNxZNX2Qh6eYn1GdzJFS4cDFfx0Q==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "MIT", - "dependencies": { - "dequal": "^2.0.3" - } - }, - "node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "license": "MIT" - }, - "node_modules/light-my-request": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/light-my-request/-/light-my-request-6.6.0.tgz", - "integrity": "sha512-CHYbu8RtboSIoVsHZ6Ye4cj4Aw/yg2oAFimlF7mNvfDV192LR7nDiKtSIfCuLT7KokPSTn/9kfVLm5OGN0A28A==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "BSD-3-Clause", - "dependencies": { - "cookie": "^1.0.1", - "process-warning": "^4.0.0", - "set-cookie-parser": "^2.6.0" - } - }, - "node_modules/light-my-request/node_modules/cookie": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", - "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/light-my-request/node_modules/process-warning": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-4.0.1.tgz", - "integrity": "sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "MIT" - }, - "node_modules/media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/on-exit-leak-free": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", - "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/pino": { - "version": "9.7.0", - "resolved": "https://registry.npmjs.org/pino/-/pino-9.7.0.tgz", - "integrity": "sha512-vnMCM6xZTb1WDmLvtG2lE/2p+t9hDEIvTWJsu6FejkE62vB7gDhvzrpFR4Cw2to+9JNQxVnkAKVPA1KPB98vWg==", - "license": "MIT", - "dependencies": { - "atomic-sleep": "^1.0.0", - "fast-redact": "^3.1.1", - "on-exit-leak-free": "^2.1.0", - "pino-abstract-transport": "^2.0.0", - "pino-std-serializers": "^7.0.0", - "process-warning": "^5.0.0", - "quick-format-unescaped": "^4.0.3", - "real-require": "^0.2.0", - "safe-stable-stringify": "^2.3.1", - "sonic-boom": "^4.0.1", - "thread-stream": "^3.0.0" - }, - "bin": { - "pino": "bin.js" - } - }, - "node_modules/pino-abstract-transport": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-2.0.0.tgz", - "integrity": "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==", - "license": "MIT", - "dependencies": { - "split2": "^4.0.0" - } - }, - "node_modules/pino-pretty": { - "version": "13.0.0", - "resolved": "https://registry.npmjs.org/pino-pretty/-/pino-pretty-13.0.0.tgz", - "integrity": "sha512-cQBBIVG3YajgoUjo1FdKVRX6t9XPxwB9lcNJVD5GCnNM4Y6T12YYx8c6zEejxQsU0wrg9TwmDulcE9LR7qcJqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "colorette": "^2.0.7", - "dateformat": "^4.6.3", - "fast-copy": "^3.0.2", - "fast-safe-stringify": "^2.1.1", - "help-me": "^5.0.0", - "joycon": "^3.1.1", - "minimist": "^1.2.6", - "on-exit-leak-free": "^2.1.0", - "pino-abstract-transport": "^2.0.0", - "pump": "^3.0.0", - "secure-json-parse": "^2.4.0", - "sonic-boom": "^4.0.1", - "strip-json-comments": "^3.1.1" - }, - "bin": { - "pino-pretty": "bin.js" - } - }, - "node_modules/pino-pretty/node_modules/dateformat": { - "version": "4.6.3", - "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-4.6.3.tgz", - "integrity": "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "*" - } - }, - "node_modules/pino-pretty/node_modules/secure-json-parse": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.7.0.tgz", - "integrity": "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/pino-pretty/node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/pino-std-serializers": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.0.0.tgz", - "integrity": "sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA==", - "license": "MIT" - }, - "node_modules/process-warning": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz", - "integrity": "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "MIT" - }, - "node_modules/pump": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", - "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", - "dev": true, - "license": "MIT", - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "node_modules/quick-format-unescaped": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", - "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", - "license": "MIT" - }, - "node_modules/real-require": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", - "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", - "license": "MIT", - "engines": { - "node": ">= 12.13.0" - } - }, - "node_modules/require-from-string": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ret": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/ret/-/ret-0.5.0.tgz", - "integrity": "sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw==", - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/reusify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", - "license": "MIT", - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, - "node_modules/rfdc": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", - "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", - "license": "MIT" - }, "node_modules/s3proxy": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/s3proxy/-/s3proxy-3.0.0.tgz", - "integrity": "sha512-hFMTyb/LOLCnM992P3q8JokC7LoXPlYp61sUC3wl7iETFcXLyelw/dTMBwN2Y4h26939//A2JutW9oWfNT36uw==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/s3proxy/-/s3proxy-4.1.0.tgz", + "integrity": "sha512-X5BakRUgtSEBwMY2rTt8XfB+4hTxEEiPKBsD6cKMG1Ox0xYCXq/rjlhZofUrUEjARPE3kuffWrX0xrO+SlacSg==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/client-s3": "^3.832.0" @@ -2584,101 +1865,6 @@ "node": ">=22.13.0" } }, - "node_modules/safe-regex2": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/safe-regex2/-/safe-regex2-5.0.0.tgz", - "integrity": "sha512-YwJwe5a51WlK7KbOJREPdjNrpViQBI3p4T50lfwPuDhZnE3XGVTlGvi+aolc5+RvxDD6bnUmjVsU9n1eboLUYw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "MIT", - "dependencies": { - "ret": "~0.5.0" - } - }, - "node_modules/safe-stable-stringify": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", - "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/secure-json-parse": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-4.0.0.tgz", - "integrity": "sha512-dxtLJO6sc35jWidmLxo7ij+Eg48PM/kleBsxpC8QJE0qJICe+KawkDQmvCMZUr9u7WKVHgMW6vy3fQ7zMiFZMA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "BSD-3-Clause" - }, - "node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/set-cookie-parser": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", - "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==", - "license": "MIT" - }, - "node_modules/setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", - "license": "ISC" - }, - "node_modules/sonic-boom": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.0.tgz", - "integrity": "sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww==", - "license": "MIT", - "dependencies": { - "atomic-sleep": "^1.0.0" - } - }, - "node_modules/split2": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", - "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", - "license": "ISC", - "engines": { - "node": ">= 10.x" - } - }, - "node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/strnum": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.1.2.tgz", @@ -2691,67 +1877,11 @@ ], "license": "MIT" }, - "node_modules/thread-stream": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.1.0.tgz", - "integrity": "sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==", - "license": "MIT", - "dependencies": { - "real-require": "^0.2.0" - } - }, - "node_modules/toad-cache": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/toad-cache/-/toad-cache-3.7.0.tgz", - "integrity": "sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw==", - "license": "MIT", - "engines": { - "node": ">=12" - } - }, - "node_modules/toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "license": "MIT", - "engines": { - "node": ">=0.6" - } - }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" - }, - "node_modules/type-is": { - "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", - "license": "MIT", - "dependencies": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true, - "license": "ISC" } } } diff --git a/package.json b/package.json index 82b2f4d..e6c98a5 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "s3proxy-docker", - "description": "High-performance Docker container for S3Proxy with Fastify", - "version": "3.0.0", + "description": "Docker runtime for s3proxy (Hono)", + "version": "4.1.0", "type": "module", "engines": { "node": ">=22.13.0" @@ -9,33 +9,36 @@ "main": "server.js", "scripts": { "start": "node server.js", - "dev": "NODE_ENV=development LOG_LEVEL=debug node --watch server.js", + "dev": "NODE_ENV=development node --watch server.js", "test": "node --test test/*.test.js", + "test:app": "node --test test/server.test.js", + "test:container": "node --test test/docker-basic.test.js", "test:watch": "node --test --watch test/*.test.js", "test:shared": "node test/shared-integration.js", "lint": "biome check .", "lint:fix": "biome check --write .", + "credentials": "aws sts get-session-token --duration 900 > credentials.json", "docker:build": "docker build -t s3proxy-docker:latest .", "docker:run": "docker run --rm -p 8080:8080 -e BUCKET=s3proxy-public s3proxy-docker:latest", "docker:test": "docker build --target test -t s3proxy-docker:test . && docker run --rm s3proxy-docker:test", + "dockerize-for-prod-dockerhub": "docker buildx build --build-arg VERSION=$npm_package_version --push --target production -t forkzero/s3proxy:$npm_package_version -t forkzero/s3proxy:latest --platform=linux/amd64,linux/arm64 .", + "docker-login-dockerhub": "docker login --username ${DOCKERHUB_USER} --password ${DOCKERHUB_ACCESS_TOKEN}", "security:audit": "npm audit --audit-level moderate", "deps:update": "npm update", "deps:check": "npm outdated" }, "dependencies": { - "s3proxy": "^3.0.0", - "fastify": "^5.4.0", - "@fastify/helmet": "^12.0.1", - "@fastify/sensible": "^6.0.1" + "s3proxy": "^4.1.0", + "hono": "^4.12.15", + "@hono/node-server": "^2.0.0" }, "devDependencies": { - "@biomejs/biome": "^1.9.4", - "pino-pretty": "^13.0.0" + "@biomejs/biome": "^1.9.4" }, "keywords": [ "s3", "proxy", - "fastify", + "hono", "docker", "aws", "streaming", diff --git a/server.js b/server.js index 2f4ea5e..dd51b4d 100644 --- a/server.js +++ b/server.js @@ -1,238 +1,127 @@ #!/usr/bin/env node /* - S3Proxy Fastify Production Server - v3.0.0 + S3Proxy Hono Server + + Docker runtime for s3proxy (https://github.com/gmoon/s3proxy) v4. + Streams objects from an S3 bucket over HTTP using the v4 `proxy.fetch()` + API and Hono on top of @hono/node-server. + + Start: PORT=8080 BUCKET=my-bucket node server.js - High-performance production server using Fastify and s3proxy - Start: PORT=8080 node server.js - Author: George Moon */ import fs from 'node:fs' import process from 'node:process' -import Fastify from 'fastify' +import { Readable } from 'node:stream' +import { serve } from '@hono/node-server' +import { Hono } from 'hono' import { S3Proxy } from 's3proxy' -// Environment validation -const requiredEnvVars = ['BUCKET', 'PORT'] -const missingVars = requiredEnvVars.filter((varName) => !process.env[varName]) +const PORT = Number(process.env.PORT) || 8080 +const BUCKET = process.env.BUCKET +const NODE_ENV = process.env.NODE_ENV || 'production' +const S3PROXY_VERSION = S3Proxy.version() -if (missingVars.length > 0) { - console.error(`โŒ Missing required environment variables: ${missingVars.join(', ')}`) +if (!BUCKET) { + console.error('โŒ Missing required environment variable: BUCKET') process.exit(1) } -const { BUCKET, PORT, NODE_ENV = 'production', LOG_LEVEL = 'info' } = process.env - -// Fastify instance with optimized configuration -const fastify = Fastify({ - logger: { - level: LOG_LEVEL, - ...(NODE_ENV === 'production' - ? { - // Structured JSON logging for production - serializers: { - req: (req) => ({ - method: req.method, - url: req.url, - hostname: req.hostname, - remoteAddress: req.ip, - userAgent: req.headers['user-agent'], - }), - res: (res) => ({ - statusCode: res.statusCode, - responseTime: res.responseTime, - }), - }, - } - : { - // Pretty printing for development - transport: { - target: 'pino-pretty', - options: { - colorize: true, - translateTime: 'SYS:standard', - }, - }, - }), - }, - trustProxy: true, - disableRequestLogging: false, - requestIdHeader: 'x-request-id', - requestIdLogLabel: 'reqId', -}) - -// Graceful shutdown handling -const gracefulShutdown = async (signal) => { - fastify.log.info(`Received ${signal}, shutting down gracefully...`) - try { - await fastify.close() - fastify.log.info('Server closed successfully') - process.exit(0) - } catch (err) { - fastify.log.error('Error during shutdown:', err) - process.exit(1) - } -} - -process.on('SIGTERM', () => gracefulShutdown('SIGTERM')) -process.on('SIGINT', () => gracefulShutdown('SIGINT')) - -// Credential management (dev vs production) +// Development credential handling. In production the AWS SDK credential chain is +// used (instance role, env vars, etc.). In development a temporary credentials +// file can be mounted at the working directory (see `npm run credentials`). function getCredentials() { - const credentialsFile = './credentials.json' - + if (/^prod/i.test(NODE_ENV)) { + return undefined + } + const file = './credentials.json' try { - if (NODE_ENV?.match(/^prod/i)) { - fastify.log.info('Production mode: using AWS SDK credential chain') - return undefined - } - - const credentialsData = JSON.parse(fs.readFileSync(credentialsFile, 'utf8')) - const credentials = credentialsData.Credentials - - fastify.log.info(`Development mode: using credentials from ${credentialsFile}`) + const { Credentials } = JSON.parse(fs.readFileSync(file, 'utf8')) + console.log(`using credentials from ${file}`) return { - accessKeyId: credentials.AccessKeyId, - secretAccessKey: credentials.SecretAccessKey, - sessionToken: credentials.SessionToken, + accessKeyId: Credentials.AccessKeyId, + secretAccessKey: Credentials.SecretAccessKey, + sessionToken: Credentials.SessionToken, } - } catch (_error) { - fastify.log.info('Using AWS SDK credential chain') + } catch { + console.log('using AWS SDK credential chain') return undefined } } -// Initialize S3Proxy -const credentials = getCredentials() -const proxy = new S3Proxy({ bucket: BUCKET, credentials }) +const proxy = new S3Proxy({ bucket: BUCKET, credentials: getCredentials() }) try { await proxy.init() - fastify.log.info(`S3Proxy initialized successfully for bucket: ${BUCKET}`) + console.log(`S3Proxy initialized for bucket: ${BUCKET}`) } catch (error) { - fastify.log.error(`Failed to initialize S3Proxy for bucket ${BUCKET}:`, error) + console.error(`Failed to initialize S3Proxy for bucket: ${BUCKET}`, error) process.exit(1) } -// Proxy error handling proxy.on('error', (error) => { - fastify.log.error('S3Proxy error:', error) -}) - -// Security headers plugin -await fastify.register(import('@fastify/helmet'), { - contentSecurityPolicy: { - directives: { - defaultSrc: ["'self'"], - styleSrc: ["'self'", "'unsafe-inline'"], - scriptSrc: ["'self'"], - imgSrc: ["'self'", 'data:', 'https:'], - }, - }, + console.error('S3Proxy error:', error) }) -// Request/Response time tracking -await fastify.register(import('@fastify/sensible')) - -// Health check endpoints -fastify.get('/health', async (_request, _reply) => { - return { status: 'ok', timestamp: new Date().toISOString() } -}) +const app = new Hono() -fastify.get('/health/s3', async (_request, reply) => { - try { - const _stream = await proxy.healthCheckStream(reply.raw) - return reply.hijack() - } catch (error) { - fastify.log.error('S3 health check failed:', error) - return reply.code(503).send({ - status: 'error', - message: 'S3 connectivity check failed', - timestamp: new Date().toISOString(), - }) - } +// Liveness + S3 connectivity. healthCheck() throws if the bucket is unreachable. +app.get('/health', async (c) => { + await proxy.healthCheck() + return c.json({ status: 'ok', timestamp: new Date().toISOString() }) }) -// Version endpoint -fastify.get('/version', async (_request, _reply) => { - return { - s3proxy: S3Proxy.version(), - fastify: fastify.version, +app.get('/version', (c) => + c.json({ + s3proxy: S3PROXY_VERSION, node: process.version, timestamp: new Date().toISOString(), - } -}) - -// Root redirect -fastify.get('/', async (_request, reply) => { - return reply.redirect(301, '/index.html') + }) +) + +app.get('/', (c) => c.redirect('/index.html', 301)) + +// Stream every key straight from S3. proxy.fetch() throws a typed S3ProxyError +// on classified failures (404/403/416), which is handled by app.onError below. +app.on(['GET', 'HEAD'], '/*', async (c) => { + const url = new URL(c.req.url) + const { stream, status, headers } = await proxy.fetch({ + url: url.pathname + url.search, + method: c.req.method, + headers: Object.fromEntries(c.req.raw.headers), + }) + // Node Readable โ†’ Web ReadableStream; backpressure propagates, no buffering. + const body = Readable.toWeb(stream) + return new Response(body, { status, headers }) }) -// S3 proxy routes -fastify.head('/*', async (request, reply) => { - try { - const _stream = await proxy.head(request.raw, reply.raw) - return reply.hijack() - } catch (error) { - fastify.log.error('HEAD request failed:', error) - return reply - .code(error.statusCode || 500) - .type('application/xml') - .send( - `\n${error.message}` - ) +// Render errors as XML, matching the s3proxy Express/Fastify examples. +app.onError((error, c) => { + const status = error.statusCode || 500 + const code = error.code || error.name || 'InternalError' + if (status >= 500) { + console.error(`${c.req.method} ${c.req.url} failed:`, error) + } else { + console.log(`${c.req.method} ${c.req.url} -> ${status} ${code}`) } + const xml = `\n${error.message}` + return new Response(xml, { status, headers: { 'content-type': 'application/xml' } }) }) -fastify.get('/*', async (request, reply) => { - try { - const stream = await proxy.get(request.raw, reply.raw) - - stream.on('error', (error) => { - fastify.log.error('Stream error:', error) - if (!reply.sent) { - reply - .code(error.statusCode || 500) - .type('application/xml') - .send( - `\n${error.message}` - ) - } - }) - - return reply.hijack() - } catch (error) { - fastify.log.error('GET request failed:', error) - return reply - .code(error.statusCode || 500) - .type('application/xml') - .send( - `\n${error.message}` - ) - } +const server = serve({ fetch: app.fetch, port: PORT, hostname: '0.0.0.0' }, ({ port }) => { + console.log(`๐Ÿš€ S3Proxy Hono server listening on port ${port}`) + console.log(`๐Ÿ“ฆ s3proxy version: ${S3PROXY_VERSION}`) + console.log(`๐Ÿชฃ S3 bucket: ${BUCKET}`) }) -// Start server -try { - const address = await fastify.listen({ - port: Number.parseInt(PORT, 10), - host: '0.0.0.0', - }) - - fastify.log.info(`๐Ÿš€ S3Proxy server listening on ${address}`) - fastify.log.info(`๐Ÿ“ฆ S3Proxy version: ${S3Proxy.version()}`) - fastify.log.info(`โšก Fastify version: ${fastify.version}`) - fastify.log.info(`๐Ÿชฃ S3 Bucket: ${BUCKET}`) - - // Signal readiness for process managers (PM2, Docker, etc.) - if (process.send) { - process.send('ready') - } -} catch (error) { - fastify.log.error('Failed to start server:', error) - process.exit(1) +// Graceful shutdown for container stop / orchestrator signals. +const shutdown = (signal) => { + console.log(`Received ${signal}, shutting down gracefully...`) + server.close(() => process.exit(0)) } +process.on('SIGTERM', () => shutdown('SIGTERM')) +process.on('SIGINT', () => shutdown('SIGINT')) -export default fastify +export default app diff --git a/shared-testing/test-processor.js b/shared-testing/test-processor.js index b94e365..b8b98a4 100644 --- a/shared-testing/test-processor.js +++ b/shared-testing/test-processor.js @@ -1,11 +1,10 @@ - // Test processor for Artillery -export function setupScenario(requestParams, context, ee, next) { +export function setupScenario(_requestParams, _context, _ee, next) { // Add any setup logic here return next() } -export function logResponse(requestParams, response, context, ee, next) { +export function logResponse(requestParams, response, _context, _ee, next) { if (response.statusCode >= 400) { console.log(`Response: ${response.statusCode} for ${requestParams.url}`) } diff --git a/test/docker-basic.test.js b/test/docker-basic.test.js index b9fb203..b54deea 100644 --- a/test/docker-basic.test.js +++ b/test/docker-basic.test.js @@ -1,121 +1,92 @@ #!/usr/bin/env node /* - Basic Docker container test - tests that the container builds and starts properly - This test doesn't require AWS credentials, just verifies the container structure + Container tests โ€” verify the built image is structured and hardened as + expected. These don't need AWS credentials: they check the build, the + runtime user, the file layout, and the missing-BUCKET guard. + + Run: npm test (Docker must be available) */ import assert from 'node:assert' import { spawn } from 'node:child_process' -import { describe, test } from 'node:test' -import { setTimeout } from 'node:timers/promises' - -describe('Docker Container Basic Tests', () => { - test('Docker image builds successfully', async () => { - const buildProcess = spawn('docker', ['build', '-t', 's3proxy-docker:test', '.'], { - stdio: ['pipe', 'pipe', 'pipe'], - }) - - const buildResult = await new Promise((resolve) => { - buildProcess.on('close', (code) => { - resolve(code) - }) - }) - - assert.strictEqual(buildResult, 0, 'Docker build should succeed') - }) +import { after, before, describe, test } from 'node:test' - test('Container starts and fails gracefully without AWS credentials', async () => { - // This test verifies the container starts and fails as expected without credentials - const containerProcess = spawn( - 'docker', - [ - 'run', - '--rm', - '-e', - 'BUCKET=s3proxy-public', - '-e', - 'NODE_ENV=production', - '-e', - 'LOG_LEVEL=info', - 's3proxy-docker:test', - ], - { - stdio: ['pipe', 'pipe', 'pipe'], - } - ) +// Reuse a prebuilt image when S3PROXY_IMAGE is set (CI passes the image the +// build job already produced); otherwise build one locally for the suite. +const PREBUILT_IMAGE = process.env.S3PROXY_IMAGE +const IMAGE = PREBUILT_IMAGE || 's3proxy-docker:test' +// Run a command to completion, collecting stdout+stderr and the exit code. +function run(cmd, args, { timeoutMs = 120000 } = {}) { + return new Promise((resolve, reject) => { + const child = spawn(cmd, args, { stdio: ['ignore', 'pipe', 'pipe'] }) let output = '' - containerProcess.stderr.on('data', (data) => { - output += data.toString() + child.stdout.on('data', (d) => { + output += d }) - - containerProcess.stdout.on('data', (data) => { - output += data.toString() + child.stderr.on('data', (d) => { + output += d }) - - // Wait for container to start and fail - await setTimeout(3000) - - // Kill the container if it's still running - if (!containerProcess.killed) { - containerProcess.kill('SIGTERM') - } - - await new Promise((resolve) => { - containerProcess.on('close', resolve) + const timer = setTimeout(() => { + child.kill('SIGKILL') + reject(new Error(`command timed out: ${cmd} ${args.join(' ')}`)) + }, timeoutMs) + child.on('error', reject) + child.on('close', (code) => { + clearTimeout(timer) + resolve({ code, output }) }) - - // Verify expected behavior - should fail to initialize S3Proxy - assert.ok( - output.includes('Failed to initialize S3Proxy'), - 'Should fail to initialize S3Proxy without credentials' - ) - assert.ok(output.includes('s3proxy-public'), 'Should reference the correct bucket name') + }) +} + +describe('Docker container', () => { + before(async () => { + // Build the production image once for the whole suite (unless reusing one). + if (PREBUILT_IMAGE) return + const { code, output } = await run('docker', [ + 'build', + '--target', + 'production', + '-t', + IMAGE, + '.', + ]) + assert.strictEqual(code, 0, `docker build should succeed:\n${output}`) }) - test('Container has correct file structure', async () => { - const inspectProcess = spawn( - 'docker', - ['run', '--rm', '--entrypoint', 'ls', 's3proxy-docker:test', '-la', '/app'], - { - stdio: ['pipe', 'pipe', 'pipe'], - } - ) - - let output = '' - inspectProcess.stdout.on('data', (data) => { - output += data.toString() - }) + after(async () => { + // Only remove an image this suite built; never one it was handed. + if (PREBUILT_IMAGE) return + await run('docker', ['image', 'rm', '-f', IMAGE], { timeoutMs: 30000 }).catch(() => {}) + }) - await new Promise((resolve) => { - inspectProcess.on('close', resolve) - }) + test('runs as the non-root node user', async () => { + const { code, output } = await run('docker', ['run', '--rm', '--entrypoint', 'whoami', IMAGE]) + assert.strictEqual(code, 0, output) + assert.strictEqual(output.trim(), 'node', 'container should run as the node user') + }) - // Verify expected files are present - assert.ok(output.includes('server.js'), 'Should contain server.js') - assert.ok(output.includes('package.json'), 'Should contain package.json') - assert.ok(output.includes('node_modules'), 'Should contain node_modules') + test('has the app laid out under /src', async () => { + const { output } = await run('docker', ['run', '--rm', '--entrypoint', 'ls', IMAGE, '/src']) + for (const file of ['server.js', 'package.json', 'node_modules']) { + assert.ok(output.includes(file), `/src should contain ${file}:\n${output}`) + } }) - test('Container runs as non-root user', async () => { - const userProcess = spawn( - 'docker', - ['run', '--rm', '--entrypoint', 'whoami', 's3proxy-docker:test'], - { - stdio: ['pipe', 'pipe', 'pipe'], - } + test('exits with a clear error when BUCKET is missing', async () => { + const { code, output } = await run('docker', [ + 'run', + '--rm', + '--entrypoint', + 'node', + IMAGE, + 'server.js', + ]) + assert.notStrictEqual(code, 0, 'should exit non-zero without BUCKET') + assert.ok( + output.includes('Missing required environment variable: BUCKET'), + `should report the missing BUCKET:\n${output}` ) - - let output = '' - userProcess.stdout.on('data', (data) => { - output += data.toString() - }) - - await new Promise((resolve) => { - userProcess.on('close', resolve) - }) - - assert.strictEqual(output.trim(), 's3proxy', 'Should run as s3proxy user') }) }) diff --git a/test/server.test.js b/test/server.test.js index 373800c..c0942d7 100644 --- a/test/server.test.js +++ b/test/server.test.js @@ -61,7 +61,6 @@ describe('S3Proxy Docker Server', () => { assert.strictEqual(response.status, 200) assert.ok(data.s3proxy) - assert.ok(data.fastify) assert.ok(data.node) assert.ok(data.timestamp) }) @@ -73,25 +72,10 @@ describe('S3Proxy Docker Server', () => { assert.strictEqual(response.headers.get('location'), '/index.html') }) - test('s3 health check handles missing bucket gracefully', async () => { - const response = await fetch(`${BASE_URL}/health/s3`) - - // Should handle missing bucket gracefully - assert.ok(response.status === 200 || response.status === 503) - }) - test('server handles 404 for non-existent files', async () => { const response = await fetch(`${BASE_URL}/non-existent-file.txt`) assert.strictEqual(response.status, 404) assert.strictEqual(response.headers.get('content-type'), 'application/xml') }) - - test('server sets security headers', async () => { - const response = await fetch(`${BASE_URL}/health`) - - // Check for security headers from @fastify/helmet - assert.ok(response.headers.get('x-content-type-options')) - assert.ok(response.headers.get('x-frame-options')) - }) }) diff --git a/test/shared-integration.js b/test/shared-integration.js index d37e940..247b256 100644 --- a/test/shared-integration.js +++ b/test/shared-integration.js @@ -9,8 +9,8 @@ import { spawn } from 'node:child_process' import { existsSync } from 'node:fs' -import { setTimeout } from 'node:timers/promises' import path from 'node:path' +import { setTimeout } from 'node:timers/promises' const DOCKER_PORT = 8082 const SHARED_TESTING_PATH = '../s3proxy/shared-testing' @@ -30,29 +30,39 @@ Please ensure the main s3proxy repository is available at ../s3proxy/`) // Start Docker container for testing async function startDockerContainer() { console.log('๐Ÿณ Starting Docker container for shared testing...') - + // Stop any existing container await new Promise((resolve) => { const stopProcess = spawn('docker', ['stop', CONTAINER_NAME], { stdio: 'pipe' }) stopProcess.on('close', () => resolve()) }) - + await new Promise((resolve) => { const rmProcess = spawn('docker', ['rm', CONTAINER_NAME], { stdio: 'pipe' }) rmProcess.on('close', () => resolve()) }) - - const dockerProcess = spawn('docker', [ - 'run', '-d', - '--name', CONTAINER_NAME, - '-p', `${DOCKER_PORT}:8080`, - '-e', 'BUCKET=s3proxy-public', - '-e', 'NODE_ENV=production', - '-e', 'LOG_LEVEL=warn', - 's3proxy-docker:latest' - ], { - stdio: ['pipe', 'pipe', 'pipe'] - }) + + const dockerProcess = spawn( + 'docker', + [ + 'run', + '-d', + '--name', + CONTAINER_NAME, + '-p', + `${DOCKER_PORT}:8080`, + '-e', + 'BUCKET=s3proxy-public', + '-e', + 'NODE_ENV=production', + '-e', + 'LOG_LEVEL=warn', + 's3proxy-docker:latest', + ], + { + stdio: ['pipe', 'pipe', 'pipe'], + } + ) await new Promise((resolve, reject) => { dockerProcess.on('close', (code) => { @@ -64,64 +74,78 @@ async function startDockerContainer() { // Wait for container to be ready console.log('โณ Waiting for container to be ready...') await setTimeout(5000) - + // Check if container is running - const psProcess = spawn('docker', ['ps', '--filter', `name=${CONTAINER_NAME}`, '--format', '{{.Names}}'], { - stdio: ['pipe', 'pipe', 'pipe'] - }) - + const psProcess = spawn( + 'docker', + ['ps', '--filter', `name=${CONTAINER_NAME}`, '--format', '{{.Names}}'], + { + stdio: ['pipe', 'pipe', 'pipe'], + } + ) + let containerRunning = false psProcess.stdout.on('data', (data) => { if (data.toString().includes(CONTAINER_NAME)) { containerRunning = true } }) - + await new Promise((resolve) => { psProcess.on('close', () => resolve()) }) - + if (!containerRunning) { throw new Error('Docker container is not running') } - + console.log('โœ… Docker container started and running') } // Run Artillery test with shared configuration async function runSharedTest(configName, scenarioName) { console.log(`โšก Running shared test: ${configName} with ${scenarioName}...`) - + const sharedTestingDir = checkSharedTesting() const configPath = path.join(sharedTestingDir, 'configs', configName) const scenarioPath = path.join(sharedTestingDir, 'scenarios', scenarioName) - + if (!existsSync(configPath)) { throw new Error(`Config file not found: ${configPath}`) } - + if (!existsSync(scenarioPath)) { throw new Error(`Scenario file not found: ${scenarioPath}`) } - - return new Promise((resolve, reject) => { - const artilleryProcess = spawn('npx', [ - 'artillery', 'run', - configPath, - '--scenario', scenarioPath, - '--target', `http://localhost:${DOCKER_PORT}`, - '--output', `shared-test-${configName.replace('.yml', '')}-results.json` - ], { - stdio: 'inherit', - cwd: process.cwd() - }) + + return new Promise((resolve, _reject) => { + const artilleryProcess = spawn( + 'npx', + [ + 'artillery', + 'run', + configPath, + '--scenario', + scenarioPath, + '--target', + `http://localhost:${DOCKER_PORT}`, + '--output', + `shared-test-${configName.replace('.yml', '')}-results.json`, + ], + { + stdio: 'inherit', + cwd: process.cwd(), + } + ) artilleryProcess.on('close', (code) => { if (code === 0) { console.log(`โœ… Shared test ${configName} completed successfully`) resolve() } else { - console.log(`โš ๏ธ Shared test ${configName} completed with code ${code} (some failures expected without real S3 data)`) + console.log( + `โš ๏ธ Shared test ${configName} completed with code ${code} (some failures expected without real S3 data)` + ) resolve() // Don't reject - some failures are expected without real S3 files } }) @@ -131,20 +155,20 @@ async function runSharedTest(configName, scenarioName) { // Run basic connectivity test async function runConnectivityTest() { console.log('๐Ÿ” Running basic connectivity test...') - + const testCases = [ { path: '/health', description: 'Health check', expectSuccess: false }, // Will fail without S3 { path: '/version', description: 'Version endpoint', expectSuccess: false }, // Will fail without S3 - { path: '/nonexistent.txt', description: '404 handling', expectSuccess: false } + { path: '/nonexistent.txt', description: '404 handling', expectSuccess: false }, ] let passedTests = 0 - + for (const testCase of testCases) { try { const response = await fetch(`http://localhost:${DOCKER_PORT}${testCase.path}`) const success = response.status < 500 // Accept any non-server-error response - + if (success || !testCase.expectSuccess) { console.log(`โœ… ${testCase.description}: ${response.status}`) passedTests++ @@ -155,19 +179,21 @@ async function runConnectivityTest() { console.log(`โŒ ${testCase.description}: ${error.message}`) } } - - console.log(`๐Ÿ“Š Connectivity Results: ${passedTests}/${testCases.length} tests handled gracefully`) + + console.log( + `๐Ÿ“Š Connectivity Results: ${passedTests}/${testCases.length} tests handled gracefully` + ) } // Cleanup function async function cleanup() { console.log('๐Ÿงน Cleaning up Docker container...') - + await new Promise((resolve) => { const stopProcess = spawn('docker', ['stop', CONTAINER_NAME], { stdio: 'pipe' }) stopProcess.on('close', () => resolve()) }) - + await new Promise((resolve) => { const rmProcess = spawn('docker', ['rm', CONTAINER_NAME], { stdio: 'pipe' }) rmProcess.on('close', () => resolve()) @@ -178,43 +204,44 @@ async function cleanup() { async function main() { try { console.log('๐Ÿš€ Starting s3proxy-docker shared testing integration...\n') - + // Check prerequisites checkSharedTesting() - + // Build Docker image console.log('๐Ÿ”จ Building Docker image...') await new Promise((resolve, reject) => { const buildProcess = spawn('docker', ['build', '-t', 's3proxy-docker:latest', '.'], { - stdio: 'inherit' + stdio: 'inherit', }) buildProcess.on('close', (code) => { if (code === 0) resolve() else reject(new Error(`Docker build failed with code ${code}`)) }) }) - + // Start container await startDockerContainer() - + // Run connectivity tests await runConnectivityTest() - + // Run shared tests (these will have expected failures without real S3 data) console.log('\n๐Ÿงช Running shared testing scenarios...') console.log('Note: Some failures are expected without real S3 bucket data\n') - + // Run a basic load test to verify the container responds try { await runSharedTest('docker-container.yml', 'basic-load.yml') } catch (error) { console.log(`โš ๏ธ Shared test completed with expected errors: ${error.message}`) } - + console.log('\n๐ŸŽ‰ Shared testing integration completed!') console.log('๐Ÿ“ The container is properly integrated with shared testing infrastructure') - console.log('๐Ÿ”ง To run with real S3 data, configure AWS credentials and use bucket: s3proxy-public') - + console.log( + '๐Ÿ”ง To run with real S3 data, configure AWS credentials and use bucket: s3proxy-public' + ) } catch (error) { console.error(`\nโŒ Shared testing integration failed: ${error.message}`) process.exit(1)