master 7852e23b8f77 cached
36 files
184.7 KB
62.6k tokens
113 symbols
1 requests
Download .txt
Repository: mattermost/mattermost-push-proxy
Branch: master
Commit: 7852e23b8f77
Files: 36
Total size: 184.7 KB

Directory structure:
gitextract_wfqcrn85/

├── .github/
│   ├── dependabot.yml
│   └── workflows/
│       └── ci.yml
├── .gitignore
├── .golangci.yml
├── CHANGELOG.md
├── CONTRIBUTING.md
├── LICENSE.txt
├── Makefile
├── NOTICE.txt
├── README.md
├── bin/
│   └── .gitignore
├── build/
│   └── setup.txt
├── config/
│   └── mattermost-push-proxy.sample.json
├── docker/
│   ├── Dockerfile
│   ├── Dockerfile.fips
│   └── entrypoint
├── go.mod
├── go.sum
├── internal/
│   └── version/
│       └── version.go
├── main.go
├── package.json
├── scripts/
│   └── go_install.sh
├── server/
│   ├── android_notification_server.go
│   ├── android_notification_test.go
│   ├── apple_notification_server.go
│   ├── config_push_proxy.go
│   ├── logger.go
│   ├── logger_test.go
│   ├── metrics.go
│   ├── metrics_test.go
│   ├── push_notification.go
│   ├── push_response.go
│   ├── server.go
│   └── server_test.go
└── swagger/
    ├── spec.yaml
    └── template.hbs

================================================
FILE CONTENTS
================================================

================================================
FILE: .github/dependabot.yml
================================================
version: 2
updates:
  - package-ecosystem: "github-actions"
    directory: "/"
    reviewers:
      - "mattermost/cloud-sre"
    open-pull-requests-limit: 5
    groups:
      Github Actions updates:
        applies-to: version-updates
        dependency-type: production
    schedule:
      # Check for updates to GitHub Actions every week
      day: "monday"
      time: "10:00"
      interval: "weekly"


================================================
FILE: .github/workflows/ci.yml
================================================
name: Build and Test

on:
  push:
    branches:
      - master
  pull_request:

concurrency:
  group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
  cancel-in-progress: true

permissions:
  contents: read
  id-token: write  # Required for OIDC authentication with Chainguard identity

env:
  GO_VERSION: 1.24.6
  FIPS_ENABLED: true

jobs:
  lint:
    name: Lint
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
        with:
          fetch-depth: 0
      - name: Lint
        run: make lint

  test:
    name: Test
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
        with:
          fetch-depth: 0
      - name: Test
        run: make test

  package:
    name: Package
    runs-on: ubuntu-latest
    needs: [lint, test]
    steps:
      - name: Checkout code
        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
        with:
          fetch-depth: 0
      - name: build-package/package
        run: make package

  build-amd64:
    name: Build AMD64
    runs-on: ubuntu-latest
    needs: [lint, test]
    if: github.actor != 'dependabot[bot]'
    timeout-minutes: 30
    steps:
      - name: Checkout code
        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
        with:
          fetch-depth: 0
      - name: Build Docker Image for AMD64
        run: make build-image-amd64-with-tags

  build-arm64:
    name: Build ARM64
    runs-on: ubuntu-24.04-arm
    needs: [lint, test]
    if: github.actor != 'dependabot[bot]'
    timeout-minutes: 30
    steps:
      - name: Checkout code
        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
        with:
          fetch-depth: 0
      - name: Build Docker Image for ARM64
        run: make build-image-arm64-with-tags

  fips-build-amd64:
    name: FIPS Build AMD64
    runs-on: ubuntu-latest
    needs: [lint, test]
    if: github.actor != 'dependabot[bot]'
    timeout-minutes: 30
    permissions:
      contents: read
      id-token: write
    steps:
      - name: Checkout code
        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
        with:
          fetch-depth: 0

      - name: Setup Chainguard
        uses: chainguard-dev/setup-chainctl@c125f765e82b09a42af3185f3214465314d75c5d # v0.5.0
        with:
          identity: ${{ secrets.CHAINGUARD_IDENTITY }}

      - name: Build FIPS Docker Image for AMD64
        run: make build-image-fips-amd64-with-tags

  fips-build-arm64:
    name: FIPS Build ARM64
    runs-on: ubuntu-24.04-arm
    needs: [lint, test]
    if: github.actor != 'dependabot[bot]'
    timeout-minutes: 30
    permissions:
      contents: read
      id-token: write
    steps:
      - name: Checkout code
        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
        with:
          fetch-depth: 0

      - name: Setup Chainguard
        uses: chainguard-dev/setup-chainctl@c125f765e82b09a42af3185f3214465314d75c5d # v0.5.0
        with:
          identity: ${{ secrets.CHAINGUARD_IDENTITY }}

      - name: Build FIPS Docker Image for ARM64
        run: make build-image-fips-arm64-with-tags

  fips-security-scan:
    name: FIPS Security Scan
    runs-on: ubuntu-latest
    needs: [lint, test]
    if: github.actor != 'dependabot[bot]'
    steps:
      - name: Checkout code
        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
        with:
          fetch-depth: 0

      - name: Setup Chainguard
        uses: chainguard-dev/setup-chainctl@c125f765e82b09a42af3185f3214465314d75c5d # v0.5.0
        with:
          identity: ${{ secrets.CHAINGUARD_IDENTITY }}

      - name: Build FIPS Docker image for scanning
        run: make build-image-fips-amd64-with-tags

      - name: Run Grype vulnerability scanner
        uses: anchore/scan-action@7037fa011853d5a11690026fb85feee79f4c946c # v7.3.2
        with:
          image: "mattermost/mattermost-push-proxy-fips:${{ github.ref == 'refs/heads/master' && 'master' || format('dev-{0}', github.sha) }}"
          output-format: table
          fail-build: false

  security-scan:
    name: Security Scan
    runs-on: ubuntu-latest
    needs: [lint, test]
    if: github.actor != 'dependabot[bot]'
    steps:
      - name: Checkout code
        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
        with:
          fetch-depth: 0

      - name: Build Docker image for scanning
        run: make build-image-amd64-with-tags

      - name: Run Grype vulnerability scanner
        uses: anchore/scan-action@7037fa011853d5a11690026fb85feee79f4c946c # v7.3.2
        with:
          image: "mattermost-push-proxy:${{ github.ref == 'refs/heads/master' && 'master' || format('dev-{0}', github.sha) }}"
          output-format: table
          fail-build: false

  pr-deploy-amd64:
    name: PR Deploy AMD64
    runs-on: ubuntu-latest
    needs: [security-scan, build-amd64]
    if: github.event_name == 'pull_request' && github.repository_owner == 'mattermost' && github.actor != 'dependabot[bot]'
    permissions:
      contents: read
      id-token: write
    steps:
      - name: Checkout code
        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
        with:
          fetch-depth: 0

      - name: Login to Docker Hub
        uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
        with:
          username: matterbuild
          password: ${{ secrets.DOCKERHUB_TOKEN }}

      - name: Build and push AMD64 PR images
        run: |
          # Build with default APP_NAME (avoids filesystem issues)
          make build-image-amd64-with-tags

          # Retag with correct namespace for pushing
          docker tag mattermost-push-proxy:dev-${{ github.sha }}-amd64 mattermost/mattermost-push-proxy:dev-${{ github.sha }}-amd64
          docker tag mattermost-push-proxy:dev-${{ github.sha }} mattermost/mattermost-push-proxy:dev-${{ github.sha }}

          # Push to correct namespace
          docker push mattermost/mattermost-push-proxy:dev-${{ github.sha }}-amd64
          echo "✅ AMD64 image pushed: mattermost/mattermost-push-proxy:dev-${{ github.sha }}-amd64"

  pr-deploy-arm64:
    name: PR Deploy ARM64
    runs-on: ubuntu-24.04-arm
    needs: [security-scan, build-arm64]
    if: github.event_name == 'pull_request' && github.repository_owner == 'mattermost' && github.actor != 'dependabot[bot]'
    permissions:
      contents: read
      id-token: write
    steps:
      - name: Checkout code
        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
        with:
          fetch-depth: 0

      - name: Login to Docker Hub
        uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
        with:
          username: matterbuild
          password: ${{ secrets.DOCKERHUB_TOKEN }}

      - name: Build and push ARM64 PR images
        run: |
          # Build with default APP_NAME (avoids filesystem issues)
          make build-image-arm64-with-tags

          # Retag with correct namespace for pushing
          docker tag mattermost-push-proxy:dev-${{ github.sha }}-arm64 mattermost/mattermost-push-proxy:dev-${{ github.sha }}-arm64
          docker tag mattermost-push-proxy:dev-${{ github.sha }} mattermost/mattermost-push-proxy:dev-${{ github.sha }}

          # Push to correct namespace
          docker push mattermost/mattermost-push-proxy:dev-${{ github.sha }}-arm64
          echo "✅ ARM64 image pushed: mattermost/mattermost-push-proxy:dev-${{ github.sha }}-arm64"

  pr-deploy-manifest:
    name: PR Deploy Manifest
    runs-on: ubuntu-latest
    needs: [pr-deploy-amd64, pr-deploy-arm64]
    if: github.event_name == 'pull_request' && github.repository_owner == 'mattermost' && github.actor != 'dependabot[bot]'
    permissions:
      contents: read
      id-token: write
    steps:
      - name: Login to Docker Hub
        uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
        with:
          username: matterbuild
          password: ${{ secrets.DOCKERHUB_TOKEN }}

      - name: Create and push multi-arch manifest
        run: |
          # Create multi-platform manifest for PR testing
          docker manifest create mattermost/mattermost-push-proxy:dev-${{ github.sha }} \
            --amend mattermost/mattermost-push-proxy:dev-${{ github.sha }}-amd64 \
            --amend mattermost/mattermost-push-proxy:dev-${{ github.sha }}-arm64
          docker manifest push mattermost/mattermost-push-proxy:dev-${{ github.sha }}

          # Clean up intermediate architecture-specific tags (like production)
          echo "Cleaning up intermediate architecture-specific tags..."
          docker rmi mattermost/mattermost-push-proxy:dev-${{ github.sha }}-amd64 2>/dev/null || true
          docker rmi mattermost/mattermost-push-proxy:dev-${{ github.sha }}-arm64 2>/dev/null || true
          echo "✅ Multi-arch PR image available (arch-specific tags removed):"
          echo "  docker pull mattermost/mattermost-push-proxy:dev-${{ github.sha }}"

  pr-deploy-fips-amd64:
    name: PR Deploy FIPS AMD64
    runs-on: ubuntu-latest
    needs: [fips-security-scan, fips-build-amd64]
    if: github.event_name == 'pull_request' && github.repository_owner == 'mattermost' && github.actor != 'dependabot[bot]'
    permissions:
      contents: read
      id-token: write
    steps:
      - name: Checkout code
        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
        with:
          fetch-depth: 0

      - name: Setup Chainguard Identity
        uses: chainguard-dev/setup-chainctl@c125f765e82b09a42af3185f3214465314d75c5d # v0.5.0
        with:
          identity: ${{ secrets.CHAINGUARD_IDENTITY }}

      - name: Login to Docker Hub
        uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
        with:
          username: matterbuild
          password: ${{ secrets.DOCKERHUB_TOKEN }}

      - name: Build and push FIPS AMD64 PR images
        run: |
          # Build with default APP_NAME (avoids filesystem issues)
          make build-image-fips-amd64-with-tags

          # Retag with correct namespace for pushing
          docker tag mattermost/mattermost-push-proxy-fips:dev-${{ github.sha }}-amd64 mattermost/mattermost-push-proxy-fips:dev-${{ github.sha }}-amd64
          docker tag mattermost/mattermost-push-proxy-fips:dev-${{ github.sha }} mattermost/mattermost-push-proxy-fips:dev-${{ github.sha }}

          # Push to correct namespace
          docker push mattermost/mattermost-push-proxy-fips:dev-${{ github.sha }}-amd64
          echo "✅ FIPS AMD64 image pushed: mattermost/mattermost-push-proxy-fips:dev-${{ github.sha }}-amd64"

  pr-deploy-fips-arm64:
    name: PR Deploy FIPS ARM64
    runs-on: ubuntu-24.04-arm
    needs: [fips-security-scan, fips-build-arm64]
    if: github.event_name == 'pull_request' && github.repository_owner == 'mattermost' && github.actor != 'dependabot[bot]'
    permissions:
      contents: read
      id-token: write
    steps:
      - name: Checkout code
        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
        with:
          fetch-depth: 0

      - name: Setup Chainguard Identity
        uses: chainguard-dev/setup-chainctl@c125f765e82b09a42af3185f3214465314d75c5d # v0.5.0
        with:
          identity: ${{ secrets.CHAINGUARD_IDENTITY }}

      - name: Login to Docker Hub
        uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
        with:
          username: matterbuild
          password: ${{ secrets.DOCKERHUB_TOKEN }}

      - name: Build and push FIPS ARM64 PR images
        run: |
          # Build with default APP_NAME (avoids filesystem issues)
          make build-image-fips-arm64-with-tags

          # Retag with correct namespace for pushing
          docker tag mattermost/mattermost-push-proxy-fips:dev-${{ github.sha }}-arm64 mattermost/mattermost-push-proxy-fips:dev-${{ github.sha }}-arm64
          docker tag mattermost/mattermost-push-proxy-fips:dev-${{ github.sha }} mattermost/mattermost-push-proxy-fips:dev-${{ github.sha }}

          # Push to correct namespace
          docker push mattermost/mattermost-push-proxy-fips:dev-${{ github.sha }}-arm64
          echo "✅ FIPS ARM64 image pushed: mattermost/mattermost-push-proxy-fips:dev-${{ github.sha }}-arm64"

  pr-deploy-fips-manifest:
    name: PR Deploy FIPS Manifest
    runs-on: ubuntu-latest
    needs: [pr-deploy-fips-amd64, pr-deploy-fips-arm64]
    if: github.event_name == 'pull_request' && github.repository_owner == 'mattermost' && github.actor != 'dependabot[bot]'
    permissions:
      contents: read
      id-token: write
    steps:
      - name: Login to Docker Hub
        uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
        with:
          username: matterbuild
          password: ${{ secrets.DOCKERHUB_TOKEN }}

      - name: Create and push FIPS multi-arch manifest
        run: |
          # Create multi-platform FIPS manifest for PR testing
          docker manifest create mattermost/mattermost-push-proxy-fips:dev-${{ github.sha }} \
            --amend mattermost/mattermost-push-proxy-fips:dev-${{ github.sha }}-amd64 \
            --amend mattermost/mattermost-push-proxy-fips:dev-${{ github.sha }}-arm64
          docker manifest push mattermost/mattermost-push-proxy-fips:dev-${{ github.sha }}

          # Clean up intermediate FIPS architecture-specific tags (like production)
          echo "Cleaning up intermediate FIPS architecture-specific tags..."
          docker rmi mattermost/mattermost-push-proxy-fips:dev-${{ github.sha }}-amd64 2>/dev/null || true
          docker rmi mattermost/mattermost-push-proxy-fips:dev-${{ github.sha }}-arm64 2>/dev/null || true
          echo "✅ Multi-arch FIPS PR image available (arch-specific tags removed):"
          echo "  docker pull mattermost/mattermost-push-proxy-fips:dev-${{ github.sha }}"



  deploy-amd64:
    name: Deploy AMD64
    runs-on: ubuntu-latest
    needs: [security-scan, build-amd64]
    if: github.repository_owner == 'mattermost' && (github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/tags/v')) && github.actor != 'dependabot[bot]'
    permissions:
      contents: write
      id-token: write
    steps:
      - name: Checkout code
        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
        with:
          fetch-depth: 0

      - name: Login to Docker Hub
        uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
        with:
          username: matterbuild
          password: ${{ secrets.DOCKERHUB_TOKEN }}

      - name: Build and push AMD64 image
        run: |
          # Build AMD64 image with temp tag for cleanup
          SHORT_SHA=${GITHUB_SHA:0:7}
          make build-image-amd64-with-tags
          # Retag with temp namespace for later cleanup
          docker tag mattermost-push-proxy:master-amd64 mattermost/mattermost-push-proxy:temp-${GITHUB_SHA}-amd64
          # Push temp AMD64 image
          docker push mattermost/mattermost-push-proxy:temp-${GITHUB_SHA}-amd64

  deploy-arm64:
    name: Deploy ARM64
    runs-on: ubuntu-24.04-arm
    needs: [security-scan, build-arm64]
    if: github.repository_owner == 'mattermost' && (github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/tags/v')) && github.actor != 'dependabot[bot]'
    permissions:
      contents: write
      id-token: write
    steps:
      - name: Checkout code
        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
        with:
          fetch-depth: 0

      - name: Login to Docker Hub
        uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
        with:
          username: matterbuild
          password: ${{ secrets.DOCKERHUB_TOKEN }}

      - name: Build and push ARM64 image
        run: |
          # Build ARM64 image with temp tag for cleanup
          SHORT_SHA=${GITHUB_SHA:0:7}
          make build-image-arm64-with-tags
          # Retag with temp namespace for later cleanup
          docker tag mattermost-push-proxy:master-arm64 mattermost/mattermost-push-proxy:temp-${GITHUB_SHA}-arm64
          # Push temp ARM64 image
          docker push mattermost/mattermost-push-proxy:temp-${GITHUB_SHA}-arm64

  deploy:
    name: Deploy
    runs-on: ubuntu-latest
    needs: [deploy-amd64, deploy-arm64]
    if: github.repository_owner == 'mattermost' && (github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/tags/v')) && github.actor != 'dependabot[bot]'
    permissions:
      contents: write
      id-token: write
    steps:
      - name: Checkout code
        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
        with:
          fetch-depth: 0

      - name: Login to Docker Hub
        uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
        with:
          username: matterbuild
          password: ${{ secrets.DOCKERHUB_TOKEN }}

      - name: Create and push multi-arch manifest
        run: |
          # Create multi-platform manifest using commit SHA
          SHORT_SHA=${GITHUB_SHA:0:7}
          docker manifest create mattermost/mattermost-push-proxy:${SHORT_SHA} \
            --amend mattermost/mattermost-push-proxy:temp-${GITHUB_SHA}-amd64 \
            --amend mattermost/mattermost-push-proxy:temp-${GITHUB_SHA}-arm64
          docker manifest push mattermost/mattermost-push-proxy:${SHORT_SHA}

          echo "✅ Clean unified multi-arch tag: mattermost/mattermost-push-proxy:${SHORT_SHA}"

          # Cleanup temp tags using Docker Hub API with org-level cleanup token
          echo "🗑️  Cleaning up temp tags from Docker Hub..."

          # Delete temp tags using Docker Hub API
          TEMP_AMD64_TAG="temp-${GITHUB_SHA}-amd64"
          TEMP_ARM64_TAG="temp-${GITHUB_SHA}-arm64"

          # Get Docker Hub API token using org-level cleanup token
          DOCKER_HUB_TOKEN=$(curl -s -X POST \
            -H "Content-Type: application/json" \
            -d '{"username": "matterbuild", "password": "${{ secrets.DOCKERHUB_CLEANUP_TOKEN }}"}' \
            https://hub.docker.com/v2/users/login/ | jq -r .token)

          # Delete AMD64 temp tag
          curl -X DELETE \
            -H "Authorization: JWT ${DOCKER_HUB_TOKEN}" \
            "https://hub.docker.com/v2/repositories/mattermost/mattermost-push-proxy/tags/${TEMP_AMD64_TAG}/" \
            && echo "✅ Deleted AMD64 temp tag" || echo "⚠️  AMD64 temp tag not found or already deleted"

          # Delete ARM64 temp tag
          curl -X DELETE \
            -H "Authorization: JWT ${DOCKER_HUB_TOKEN}" \
            "https://hub.docker.com/v2/repositories/mattermost/mattermost-push-proxy/tags/${TEMP_ARM64_TAG}/" \
            && echo "✅ Deleted ARM64 temp tag" || echo "⚠️  ARM64 temp tag not found or already deleted"

          echo "✅ Temp tags cleaned up from Docker Hub"

      - name: Create release
        if: startsWith(github.ref, 'refs/tags/v')
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: make github-release

  fips-deploy-amd64:
    name: FIPS Deploy AMD64
    runs-on: ubuntu-latest
    needs: [fips-security-scan, fips-build-amd64]
    if: github.repository_owner == 'mattermost' && (github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/tags/v')) && github.actor != 'dependabot[bot]'
    permissions:
      contents: write
      id-token: write
    steps:
      - name: Checkout code
        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
        with:
          fetch-depth: 0

      - name: Setup Chainguard Identity
        uses: chainguard-dev/setup-chainctl@c125f765e82b09a42af3185f3214465314d75c5d # v0.5.0
        with:
          identity: ${{ secrets.CHAINGUARD_IDENTITY }}

      - name: Login to Docker Hub
        uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
        with:
          username: matterbuild
          password: ${{ secrets.DOCKERHUB_TOKEN }}

      - name: Build and push FIPS AMD64 image
        run: |
          # Build FIPS AMD64 image with temp tag for cleanup
          SHORT_SHA=${GITHUB_SHA:0:7}
          make build-image-fips-amd64-with-tags
          # Retag with temp namespace for later cleanup
          docker tag mattermost/mattermost-push-proxy-fips:master-amd64 mattermost/mattermost-push-proxy-fips:temp-${GITHUB_SHA}-amd64
          # Push temp FIPS AMD64 image
          docker push mattermost/mattermost-push-proxy-fips:temp-${GITHUB_SHA}-amd64

  fips-deploy-arm64:
    name: FIPS Deploy ARM64
    runs-on: ubuntu-24.04-arm
    needs: [fips-security-scan, fips-build-arm64]
    if: github.repository_owner == 'mattermost' && (github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/tags/v')) && github.actor != 'dependabot[bot]'
    permissions:
      contents: write
      id-token: write
    steps:
      - name: Checkout code
        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
        with:
          fetch-depth: 0

      - name: Setup Chainguard Identity
        uses: chainguard-dev/setup-chainctl@c125f765e82b09a42af3185f3214465314d75c5d # v0.5.0
        with:
          identity: ${{ secrets.CHAINGUARD_IDENTITY }}

      - name: Login to Docker Hub
        uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
        with:
          username: matterbuild
          password: ${{ secrets.DOCKERHUB_TOKEN }}

      - name: Build and push FIPS ARM64 image
        run: |
          # Build FIPS ARM64 image with temp tag for cleanup
          SHORT_SHA=${GITHUB_SHA:0:7}
          make build-image-fips-arm64-with-tags
          # Retag with temp namespace for later cleanup
          docker tag mattermost/mattermost-push-proxy-fips:master-arm64 mattermost/mattermost-push-proxy-fips:temp-${GITHUB_SHA}-arm64
          # Push temp FIPS ARM64 image
          docker push mattermost/mattermost-push-proxy-fips:temp-${GITHUB_SHA}-arm64

  fips-deploy:
    name: FIPS Deploy
    runs-on: ubuntu-latest
    needs: [fips-deploy-amd64, fips-deploy-arm64]
    if: github.repository_owner == 'mattermost' && (github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/tags/v')) && github.actor != 'dependabot[bot]'
    permissions:
      contents: write
      id-token: write
    steps:
      - name: Checkout code
        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
        with:
          fetch-depth: 0

      - name: Login to Docker Hub
        uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
        with:
          username: matterbuild
          password: ${{ secrets.DOCKERHUB_TOKEN }}

      - name: Create and push FIPS multi-arch manifest
        run: |
          # Create multi-platform manifest for FIPS using commit SHA
          SHORT_SHA=${GITHUB_SHA:0:7}
          docker manifest create mattermost/mattermost-push-proxy-fips:${SHORT_SHA} \
            --amend mattermost/mattermost-push-proxy-fips:temp-${GITHUB_SHA}-amd64 \
            --amend mattermost/mattermost-push-proxy-fips:temp-${GITHUB_SHA}-arm64
          docker manifest push mattermost/mattermost-push-proxy-fips:${SHORT_SHA}

          echo "✅ Clean unified FIPS multi-arch tag: mattermost/mattermost-push-proxy-fips:${SHORT_SHA}"

          # Cleanup temp FIPS tags using Docker Hub API
          echo "🗑️  Cleaning up temp FIPS tags from Docker Hub..."

          # Delete temp FIPS tags using Docker Hub API
          TEMP_AMD64_TAG="temp-${GITHUB_SHA}-amd64"
          TEMP_ARM64_TAG="temp-${GITHUB_SHA}-arm64"

          # Get Docker Hub API token using org-level cleanup token
          DOCKER_HUB_TOKEN=$(curl -s -X POST \
            -H "Content-Type: application/json" \
            -d '{"username": "matterbuild", "password": "${{ secrets.DOCKERHUB_CLEANUP_TOKEN }}"}' \
            https://hub.docker.com/v2/users/login/ | jq -r .token)

          # Delete FIPS AMD64 temp tag
          curl -X DELETE \
            -H "Authorization: JWT ${DOCKER_HUB_TOKEN}" \
            "https://hub.docker.com/v2/repositories/mattermost/mattermost-push-proxy-fips/tags/${TEMP_AMD64_TAG}/" \
            && echo "✅ Deleted FIPS AMD64 temp tag" || echo "⚠️  FIPS AMD64 temp tag not found or already deleted"

          # Delete FIPS ARM64 temp tag
          curl -X DELETE \
            -H "Authorization: JWT ${DOCKER_HUB_TOKEN}" \
            "https://hub.docker.com/v2/repositories/mattermost/mattermost-push-proxy-fips/tags/${TEMP_ARM64_TAG}/" \
            && echo "✅ Deleted FIPS ARM64 temp tag" || echo "⚠️  FIPS ARM64 temp tag not found or already deleted"

          echo "✅ Temp FIPS tags cleaned up from Docker Hub"

      - name: Create FIPS release
        if: startsWith(github.ref, 'refs/tags/v')
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: make github-release-fips

      - name: Cleanup
        run: make clean


================================================
FILE: .gitignore
================================================

logs
.DS_Store
dist

# Build Targets
.prebuild
.prebuild-go
.prebuild-jsx

# Compiled Object files, Static and Dynamic libs (Shared Objects)
*.o
*.a
*.so

# Folders
_obj
_test
.idea

# Architecture specific extensions/prefixes
[568vq].out

_testmain.go

*.exe
*.test
*.prof

# Log files
*mattermost.log

# Vim temporary files
*.swp

# Default local file storage
data/*
api/data/*

.agignore
.ctags
tags

# Certificates
*.pem
*.cer
*.p8
*.p12
*.key
certs
certs.bak
*.env

# Ignore binary
/mattermost-push-proxy

node_modules
# Swagger output file
index.html

config/mattermost-push-proxy.json
# Ignore config.json
cmd/renew_apple_cert/convert_cert/config/**
cmd/renew_apple_cert/convert_cert/config.json
cmd/renew_apple_cert/create_csr/config/**
cmd/renew_apple_cert/create_csr/config.json


================================================
FILE: .golangci.yml
================================================
# options for analysis running
run:
  # timeout for analysis, e.g. 30s, 5m, default is 1m
  timeout: 5m

linters-settings:
  gofmt:
    simplify: true
  govet:
    enable-all: true
    disable:
      - fieldalignment

linters:
  disable-all: true
  enable:
    - gofmt
    - gosimple
    - govet
    - ineffassign
    - copyloopvar
    - staticcheck
    - unconvert
    - unused


================================================
FILE: CHANGELOG.md
================================================
# Mattermost Push Proxy Changelog


## 5.8.1 Release
- Release Date: March 28, 2019
- Compatible with all versions of Mattermost server and mobile

### Compatibility
 - As of April 10, 2018, Google has deprecated the Google Cloud Messaging (GCM) service. The GCM server and client APIs are deprecated and will be removed as of April 11, 2019. You must [migrate GCM apps to Firebase Cloud Messaging (FCM)](https://developers.mattermost.com/contribute/mobile/push-notifications/migrate-gcm-fcm/), which inherits the reliable and scalable GCM infrastructure, plus many new features.
 
### Bug Fixes
 - FCM messages are sent as high priority to wake Android devices in doze mode

________________

## 5.8 Release
- Release Date: March 20, 2019
- Compatible with all versions of Mattermost server and mobile

### Combatibility
 - As of April 10, 2018, Google has deprecated the Google Cloud Messaging (GCM) service. The GCM server and client APIs are deprecated and will be removed as of April 11, 2019. You must [migrate GCM apps to Firebase Cloud Messaging (FCM)](https://developers.mattermost.com/contribute/mobile/push-notifications/migrate-gcm-fcm/), which inherits the reliable and scalable GCM infrastructure, plus many new features.
 
### Highlights
 - Adds support for Firebase Cloud Messaging


================================================
FILE: CONTRIBUTING.md
================================================
Please see: https://github.com/mattermost/mattermost-server/blob/master/CONTRIBUTING.md


================================================
FILE: LICENSE.txt
================================================
Copyright (c) 2015 Mattermost, Inc. All rights reserved.

                               Apache License
                           Version 2.0, January 2004
                        http://www.apache.org/licenses/

   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION

   1. Definitions.

      "License" shall mean the terms and conditions for use, reproduction,
      and distribution as defined by Sections 1 through 9 of this document.

      "Licensor" shall mean the copyright owner or entity authorized by
      the copyright owner that is granting the License.

      "Legal Entity" shall mean the union of the acting entity and all
      other entities that control, are controlled by, or are under common
      control with that entity. For the purposes of this definition,
      "control" means (i) the power, direct or indirect, to cause the
      direction or management of such entity, whether by contract or
      otherwise, or (ii) ownership of fifty percent (50%) or more of the
      outstanding shares, or (iii) beneficial ownership of such entity.

      "You" (or "Your") shall mean an individual or Legal Entity
      exercising permissions granted by this License.

      "Source" form shall mean the preferred form for making modifications,
      including but not limited to software source code, documentation
      source, and configuration files.

      "Object" form shall mean any form resulting from mechanical
      transformation or translation of a Source form, including but
      not limited to compiled object code, generated documentation,
      and conversions to other media types.

      "Work" shall mean the work of authorship, whether in Source or
      Object form, made available under the License, as indicated by a
      copyright notice that is included in or attached to the work
      (an example is provided in the Appendix below).

      "Derivative Works" shall mean any work, whether in Source or Object
      form, that is based on (or derived from) the Work and for which the
      editorial revisions, annotations, elaborations, or other modifications
      represent, as a whole, an original work of authorship. For the purposes
      of this License, Derivative Works shall not include works that remain
      separable from, or merely link (or bind by name) to the interfaces of,
      the Work and Derivative Works thereof.

      "Contribution" shall mean any work of authorship, including
      the original version of the Work and any modifications or additions
      to that Work or Derivative Works thereof, that is intentionally
      submitted to Licensor for inclusion in the Work by the copyright owner
      or by an individual or Legal Entity authorized to submit on behalf of
      the copyright owner. For the purposes of this definition, "submitted"
      means any form of electronic, verbal, or written communication sent
      to the Licensor or its representatives, including but not limited to
      communication on electronic mailing lists, source code control systems,
      and issue tracking systems that are managed by, or on behalf of, the
      Licensor for the purpose of discussing and improving the Work, but
      excluding communication that is conspicuously marked or otherwise
      designated in writing by the copyright owner as "Not a Contribution."

      "Contributor" shall mean Licensor and any individual or Legal Entity
      on behalf of whom a Contribution has been received by Licensor and
      subsequently incorporated within the Work.

   2. Grant of Copyright License. Subject to the terms and conditions of
      this License, each Contributor hereby grants to You a perpetual,
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
      copyright license to reproduce, prepare Derivative Works of,
      publicly display, publicly perform, sublicense, and distribute the
      Work and such Derivative Works in Source or Object form.

   3. Grant of Patent License. Subject to the terms and conditions of
      this License, each Contributor hereby grants to You a perpetual,
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
      (except as stated in this section) patent license to make, have made,
      use, offer to sell, sell, import, and otherwise transfer the Work,
      where such license applies only to those patent claims licensable
      by such Contributor that are necessarily infringed by their
      Contribution(s) alone or by combination of their Contribution(s)
      with the Work to which such Contribution(s) was submitted. If You
      institute patent litigation against any entity (including a
      cross-claim or counterclaim in a lawsuit) alleging that the Work
      or a Contribution incorporated within the Work constitutes direct
      or contributory patent infringement, then any patent licenses
      granted to You under this License for that Work shall terminate
      as of the date such litigation is filed.

   4. Redistribution. You may reproduce and distribute copies of the
      Work or Derivative Works thereof in any medium, with or without
      modifications, and in Source or Object form, provided that You
      meet the following conditions:

      (a) You must give any other recipients of the Work or
          Derivative Works a copy of this License; and

      (b) You must cause any modified files to carry prominent notices
          stating that You changed the files; and

      (c) You must retain, in the Source form of any Derivative Works
          that You distribute, all copyright, patent, trademark, and
          attribution notices from the Source form of the Work,
          excluding those notices that do not pertain to any part of
          the Derivative Works; and

      (d) If the Work includes a "NOTICE" text file as part of its
          distribution, then any Derivative Works that You distribute must
          include a readable copy of the attribution notices contained
          within such NOTICE file, excluding those notices that do not
          pertain to any part of the Derivative Works, in at least one
          of the following places: within a NOTICE text file distributed
          as part of the Derivative Works; within the Source form or
          documentation, if provided along with the Derivative Works; or,
          within a display generated by the Derivative Works, if and
          wherever such third-party notices normally appear. The contents
          of the NOTICE file are for informational purposes only and
          do not modify the License. You may add Your own attribution
          notices within Derivative Works that You distribute, alongside
          or as an addendum to the NOTICE text from the Work, provided
          that such additional attribution notices cannot be construed
          as modifying the License.

      You may add Your own copyright statement to Your modifications and
      may provide additional or different license terms and conditions
      for use, reproduction, or distribution of Your modifications, or
      for any such Derivative Works as a whole, provided Your use,
      reproduction, and distribution of the Work otherwise complies with
      the conditions stated in this License.

   5. Submission of Contributions. Unless You explicitly state otherwise,
      any Contribution intentionally submitted for inclusion in the Work
      by You to the Licensor shall be under the terms and conditions of
      this License, without any additional terms or conditions.
      Notwithstanding the above, nothing herein shall supersede or modify
      the terms of any separate license agreement you may have executed
      with Licensor regarding such Contributions.

   6. Trademarks. This License does not grant permission to use the trade
      names, trademarks, service marks, or product names of the Licensor,
      except as required for reasonable and customary use in describing the
      origin of the Work and reproducing the content of the NOTICE file.

   7. Disclaimer of Warranty. Unless required by applicable law or
      agreed to in writing, Licensor provides the Work (and each
      Contributor provides its Contributions) on an "AS IS" BASIS,
      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
      implied, including, without limitation, any warranties or conditions
      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
      PARTICULAR PURPOSE. You are solely responsible for determining the
      appropriateness of using or redistributing the Work and assume any
      risks associated with Your exercise of permissions under this License.

   8. Limitation of Liability. In no event and under no legal theory,
      whether in tort (including negligence), contract, or otherwise,
      unless required by applicable law (such as deliberate and grossly
      negligent acts) or agreed to in writing, shall any Contributor be
      liable to You for damages, including any direct, indirect, special,
      incidental, or consequential damages of any character arising as a
      result of this License or out of the use or inability to use the
      Work (including but not limited to damages for loss of goodwill,
      work stoppage, computer failure or malfunction, or any and all
      other commercial damages or losses), even if such Contributor
      has been advised of the possibility of such damages.

   9. Accepting Warranty or Additional Liability. While redistributing
      the Work or Derivative Works thereof, You may choose to offer,
      and charge a fee for, acceptance of support, warranty, indemnity,
      or other liability obligations and/or rights consistent with this
      License. However, in accepting such obligations, You may act only
      on Your own behalf and on Your sole responsibility, not on behalf
      of any other Contributor, and only if You agree to indemnify,
      defend, and hold each Contributor harmless for any liability
      incurred by, or claims asserted against, such Contributor by reason
      of your accepting any such warranty or additional liability.

   END OF TERMS AND CONDITIONS

   APPENDIX: How to apply the Apache License to your work.

      To apply the Apache License to your work, attach the following
      boilerplate notice, with the fields enclosed by brackets "[]"
      replaced with your own identifying information. (Don't include
      the brackets!)  The text should be enclosed in the appropriate
      comment syntax for the file format. We also recommend that a
      file or class name and description of purpose be included on the
      same "printed page" as the copyright notice for easier
      identification within third-party archives.

   Copyright [yyyy] [name of copyright owner]

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.


================================================
FILE: Makefile
================================================
# ====================================================================================
# Variables

## General Variables

# Branch Variables
PROTECTED_BRANCH := master
CURRENT_BRANCH   := $(shell git rev-parse --abbrev-ref HEAD)
# Use repository name as application name
APP_NAME    := $(shell basename -s .git `git config --get remote.origin.url`)
APP_COMMIT  := $(shell git rev-parse HEAD)
# Check if we are in protected branch, if yes use `protected_branch_name-sha` as app version.
# Else check if we are in a release tag, if yes use the tag as app version, else use `dev-sha` as app version.
APP_VERSION ?= $(shell if [ $(PROTECTED_BRANCH) = $(CURRENT_BRANCH) ]; then echo $(PROTECTED_BRANCH); else (git describe --abbrev=0 --exact-match --tags 2>/dev/null || echo dev-$(APP_COMMIT)) ; fi)
APP_VERSION_NO_V := $(patsubst v%,%,$(APP_VERSION))
GIT_VERSION ?= $(shell git describe --tags --always --dirty)
GIT_TREESTATE = clean
DIFF = $(shell git diff --quiet >/dev/null 2>&1; if [ $$? -eq 1 ]; then echo "1"; fi)
ifeq ($(DIFF), 1)
    GIT_TREESTATE = dirty
endif

GO_INSTALL = ./scripts/go_install.sh
TOOLS_BIN_DIR := $(abspath bin)

# Get current date and format like: 2022-04-27 11:32
BUILD_DATE  := $(shell date +%Y-%m-%d\ %H:%M)

# Get version information for plugins that depend on a semver version
BUILD_HASH = $(shell git rev-parse --short HEAD)
BUILD_TAG_LATEST = $(shell git describe --tags --match 'v*' --abbrev=0)
BUILD_TAG_CURRENT = $(shell git tag --points-at HEAD)

## General Configuration Variables
# We don't need make's built-in rules.
MAKEFLAGS     += --no-builtin-rules
# Be pedantic about undefined variables.
MAKEFLAGS     += --warn-undefined-variables
# Set help as default target
.DEFAULT_GOAL := help

# App Code location
CONFIG_APP_CODE         += ./

## Docker Variables
# Docker executable
DOCKER                  := $(shell which docker)
# Dockerfile's location
DOCKER_FILE             ?= ./docker/Dockerfile
# Docker options to inherit for all docker run commands
DOCKER_OPTS             += --rm --platform "linux/amd64"
# Registry to upload images
DOCKER_REGISTRY         ?= docker.io
DOCKER_REGISTRY_REPO    ?= mattermost/${APP_NAME}-daily
# Registry credentials
DOCKER_USER             ?= user
DOCKER_PASSWORD         ?= password
## Latest Docker tags
# if we are on a latest semver APP_VERSION tag, also push latest
ifneq ($(shell echo $(APP_VERSION) | egrep '^v([0-9]+\.){0,2}(\*|[0-9]+)'),)
  ifeq ($(shell git tag -l --sort=v:refname | tail -n1),$(APP_VERSION))
		LATEST_DOCKER_TAG = -t $(DOCKER_REGISTRY)/${DOCKER_REGISTRY_REPO}:latest
  endif
endif

## Docker Images
DOCKER_IMAGE_GO         ?= "golang:${GO_VERSION}"
DOCKER_IMAGE_GOLINT     ?= "golangci/golangci-lint:v1.64.4@sha256:e83b903d722c12402c9d88948a6cac42ea0e34bf336fc6a170ade9adeecb2d0e"
DOCKER_IMAGE_DOCKERLINT ?= "hadolint/hadolint:v2.12.0"
DOCKER_IMAGE_COSIGN     ?= "bitnami/cosign:1.8.0@sha256:8c2c61c546258fffff18b47bb82a65af6142007306b737129a7bd5429d53629a"
DOCKER_IMAGE_GH_CLI     ?= "ghcr.io/supportpal/github-gh-cli:2.31.0@sha256:71371e36e62bd24ddd42d9e4c720a7e9954cb599475e24d1407af7190e2a5685"

# To build FIPS-compliant push-proxy: make build-fips
# Requires Docker to be installed and running
FIPS_ENABLED ?= false
BUILD_IMAGE_FIPS ?= cgr.dev/mattermost.com/go-msft-fips:1.24.6
BASE_IMAGE_FIPS ?= cgr.dev/mattermost.com/glibc-openssl-fips:15.1

## Cosign Variables
# The public key
COSIGN_PUBLIC_KEY       ?= akey
# The private key
COSIGN_KEY              ?= akey
# The passphrase used to decrypt the private key
COSIGN_PASSWORD         ?= password

## Go Variables
# Go executable
GO                           := $(shell which go)
# Extract GO version from go.mod file
GO_VERSION                   ?= $(shell grep -E '^go' go.mod | awk {'print $$2'})
# LDFLAGS
GO_LDFLAGS                   += -X "github.com/mattermost/${APP_NAME}/internal/version.gitVersion=$(GIT_VERSION)"
GO_LDFLAGS                   += -X "github.com/mattermost/${APP_NAME}/internal/version.buildHash=$(BUILD_HASH)"
GO_LDFLAGS                   += -X "github.com/mattermost/${APP_NAME}/internal/version.buildTagLatest=$(BUILD_TAG_LATEST)"
GO_LDFLAGS                   += -X "github.com/mattermost/${APP_NAME}/internal/version.buildTagCurrent=$(BUILD_TAG_CURRENT)"
GO_LDFLAGS                   += -X "github.com/mattermost/${APP_NAME}/internal/version.gitTreeState=$(GIT_TREESTATE)"
GO_LDFLAGS                   += -X "github.com/mattermost/${APP_NAME}/internal/version.buildDate=$(BUILD_DATE)"

# Architectures to build for
GO_BUILD_PLATFORMS           ?= linux-amd64 linux-arm64 freebsd-amd64
GO_BUILD_PLATFORMS_ARTIFACTS = $(foreach cmd,$(addprefix go-build/,${APP_NAME}),$(addprefix $(cmd)-,$(GO_BUILD_PLATFORMS)))

# Build options
GO_BUILD_OPTS                += -trimpath
GO_TEST_OPTS                 += -v -timeout=180s
# Temporary folder to output compiled binaries artifacts
GO_OUT_BIN_DIR               := ./dist

## Github Variables
# A github access token that provides access to upload artifacts under releases
GITHUB_TOKEN                 ?= a_token
# Github organization
GITHUB_ORG                   := mattermost
# Most probably the name of the repo
GITHUB_REPO                  := ${APP_NAME}

## FIPS Docker Variables
APP_NAME_FIPS ?= mattermost/mattermost-push-proxy-fips
APP_VERSION_FIPS ?= $(APP_VERSION)-fips
APP_VERSION_NO_V_FIPS ?= $(APP_VERSION_NO_V)-fips

## Architecture Variables (default to ARM64 for local builds)
TARGET_OS ?= linux
TARGET_ARCH ?= arm64

OUTDATED_VER := master
OUTDATED_BIN := go-mod-outdated
OUTDATED_GEN := $(TOOLS_BIN_DIR)/$(OUTDATED_BIN)

# ====================================================================================
# Colors

BLUE   := $(shell printf "\033[34m")
YELLOW := $(shell printf "\033[33m")
RED    := $(shell printf "\033[31m")
GREEN  := $(shell printf "\033[32m")
CYAN   := $(shell printf "\033[36m")
CNone  := $(shell printf "\033[0m")

# ====================================================================================
# Logger

TIME_LONG	= `date +%Y-%m-%d' '%H:%M:%S`
TIME_SHORT	= `date +%H:%M:%S`
TIME		= $(TIME_SHORT)

INFO = echo ${TIME} ${BLUE}[ .. ]${CNone}
WARN = echo ${TIME} ${YELLOW}[WARN]${CNone}
ERR  = echo ${TIME} ${RED}[FAIL]${CNone}
OK   = echo ${TIME} ${GREEN}[ OK ]${CNone}
FAIL = (echo ${TIME} ${RED}[FAIL]${CNone} && false)

# ====================================================================================
# Verbosity control hack

VERBOSE ?= 0
AT_0 := @
AT_1 :=
AT = $(AT_$(VERBOSE))

# ====================================================================================
# Used for semver bumping
CURRENT_VERSION := $(shell git describe --abbrev=0 --tags)
VERSION_PARTS := $(subst ., ,$(subst v,,$(CURRENT_VERSION)))
MAJOR := $(word 1,$(VERSION_PARTS))
MINOR := $(word 2,$(VERSION_PARTS))
PATCH := $(word 3,$(VERSION_PARTS))

# Check if current branch is protected
define check_protected_branch
	@current_branch=$$(git rev-parse --abbrev-ref HEAD); \
	if ! echo "$(PROTECTED_BRANCH)" | grep -wq "$$current_branch"; then \
		echo "Error: Tagging is only allowed from $(PROTECTED_BRANCH) branch. You are on $$current_branch branch."; \
		exit 1; \
	fi
endef
# Check if there are pending pulls
define check_pending_pulls
	@git fetch; \
	current_branch=$$(git rev-parse --abbrev-ref HEAD); \
	if [ "$$(git rev-parse HEAD)" != "$$(git rev-parse origin/$$current_branch)" ]; then \
		echo "Error: Your branch is not up to date with upstream. Please pull the latest changes before performing a release"; \
		exit 1; \
	fi
endef
# ====================================================================================
# Targets

help: ## to get help
	@echo "Usage:"
	@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) |\
	awk 'BEGIN {FS = ":.*?## "}; {printf "make ${CYAN}%-30s${CNone} %s\n", $$1, $$2}'

.PHONY: build
build: go-build-docker ## to build

.PHONY: build-all
build-all: build build-fips ## to build both normal and FIPS versions

.PHONY: release
release: build github-release ## to build and release artifacts

.PHONY: release-fips
release-fips: build-fips ## to build and release FIPS artifacts

.PHONY: release-all
release-all: build-all github-release-all ## to build and release both versions

.PHONY: package
package: go-build package-software ## to build, package

.PHONY: package-fips
package-fips: build-fips ## to build FIPS version

.PHONY: package-all
package-all: package package-fips ## to build, package both versions

.PHONY: sign
sign: docker-sign docker-verify ## to sign the artifact and perform verification

.PHONY: lint
lint: go-lint docker-lint ## to lint

.PHONY: test
test: go-test ## to test


.PHONY: patch minor major

patch: ## to bump patch version (semver)
	$(call check_protected_branch)
	$(call check_pending_pulls)
	@$(eval PATCH := $(shell echo $$(($(PATCH)+1))))
	@$(INFO) Bumping $(APP_NAME) to Patch version $(MAJOR).$(MINOR).$(PATCH)
	git tag -s -a v$(MAJOR).$(MINOR).$(PATCH) -m "Bumping $(APP_NAME) to Patch version $(MAJOR).$(MINOR).$(PATCH)"
	git push origin v$(MAJOR).$(MINOR).$(PATCH)
	@$(OK) Bumping $(APP_NAME) to Patch version $(MAJOR).$(MINOR).$(PATCH)

minor: ## to bump minor version (semver)
	$(call check_protected_branch)
	$(call check_pending_pulls)
	@$(eval MINOR := $(shell echo $$(($(MINOR)+1))))
	@$(eval PATCH := 0)
	@$(INFO) Bumping $(APP_NAME) to Minor version $(MAJOR).$(MINOR).$(PATCH)
	git tag -s -a v$(MAJOR).$(MINOR).$(PATCH) -m "Bumping $(APP_NAME) to Minor version $(MAJOR).$(MINOR).$(PATCH)"
	git push origin v$(MAJOR).$(MINOR).$(PATCH)
	@$(OK) Bumping $(APP_NAME) to Minor version $(MAJOR).$(MINOR).$(PATCH)

major: ## to bump major version (semver)
	$(call check_protected_branch)
	$(call check_pending_pulls)
	$(eval MAJOR := $(shell echo $$(($(MAJOR)+1))))
	$(eval MINOR := 0)
	$(eval PATCH := 0)
	@$(INFO) Bumping $(APP_NAME) to Major version $(MAJOR).$(MINOR).$(PATCH)
	git tag -s -a v$(MAJOR).$(MINOR).$(PATCH) -m "Bumping $(APP_NAME) to Major version $(MAJOR).$(MINOR).$(PATCH)"
	git push origin v$(MAJOR).$(MINOR).$(PATCH)
	@$(OK) Bumping $(APP_NAME) to Major version $(MAJOR).$(MINOR).$(PATCH)

package-software:  ## to package the binary
	@$(INFO) Packaging
	$(AT) for file in $(GO_OUT_BIN_DIR)/mattermost-push-proxy-*; do \
		[[ "$$file" == *.tar.gz ]] && continue; \
		target=$$(basename $$file); \
		mkdir -p $(GO_OUT_BIN_DIR)/$${target}_temp/bin; \
		cp -RL config $(GO_OUT_BIN_DIR)/$${target}_temp/config; \
		echo $(APP_VERSION) > $(GO_OUT_BIN_DIR)/$${target}_temp/config/build.txt; \
		cp LICENSE.txt NOTICE.txt README.md $(GO_OUT_BIN_DIR)/$${target}_temp; \
		mkdir $(GO_OUT_BIN_DIR)/$${target}_temp/logs; \
		mv $$file $(GO_OUT_BIN_DIR)/$${target}_temp/bin/mattermost-push-proxy; \
		mv $(GO_OUT_BIN_DIR)/$${target}_temp $(GO_OUT_BIN_DIR)/$${target}; \
		tar -czf $(GO_OUT_BIN_DIR)/$${target}.tar.gz -C $(GO_OUT_BIN_DIR) $${target}; \
		rm -r $(GO_OUT_BIN_DIR)/$${target}; \
	done
	@$(OK) Packaging

.PHONY: package-software-fips
package-software-fips:  ## to package the FIPS binary
	@$(INFO) Packaging FIPS
	$(AT) for file in $(GO_OUT_BIN_DIR)/mattermost-push-proxy-*-fips; do \
		[[ "$$file" == *.tar.gz ]] && continue; \
		target=$$(basename $$file); \
		mkdir -p $(GO_OUT_BIN_DIR)/$${target}_temp/bin; \
		cp -RL config $(GO_OUT_BIN_DIR)/$${target}_temp/config; \
		echo $(APP_VERSION)-fips > $(GO_OUT_BIN_DIR)/$${target}_temp/config/build.txt; \
		cp LICENSE.txt NOTICE.txt README.md $(GO_OUT_BIN_DIR)/$${target}_temp; \
		mkdir $(GO_OUT_BIN_DIR)/$${target}_temp/logs; \
		mv $$file $(GO_OUT_BIN_DIR)/$${target}_temp/bin/mattermost-push-proxy; \
		mv $(GO_OUT_BIN_DIR)/$${target}_temp $(GO_OUT_BIN_DIR)/$${target}; \
		tar -czf $(GO_OUT_BIN_DIR)/$${target}.tar.gz -C $(GO_OUT_BIN_DIR) $${target}; \
		rm -r $(GO_OUT_BIN_DIR)/$${target}; \
	done
	@$(OK) Packaging FIPS

.PHONY: docker-build
docker-build: ## to build the docker image
	@$(INFO) Performing Docker build ${APP_NAME}:${APP_VERSION_NO_V}
	$(AT)$(DOCKER) buildx build \
	--no-cache --pull --platform linux/amd64,linux/arm64 \
	-f ${DOCKER_FILE} . \
	-t ${APP_NAME}:${APP_VERSION_NO_V} || ${FAIL}
	@$(OK) Performing Docker build ${APP_NAME}:${APP_VERSION_NO_V}

## --------------------------------------
## Regular Multi-Architecture Build Targets
## --------------------------------------

.PHONY: build-image-amd64-with-tags
build-image-amd64-with-tags: go-build-amd64 package-software ## Build Docker image for AMD64 with tags
	@echo "Building mattermost-push-proxy Docker Image for AMD64"
	docker build --no-cache --pull \
		--build-arg TARGETOS=linux \
		--build-arg TARGETARCH=amd64 \
		-f ${DOCKER_FILE} \
		-t ${APP_NAME}:${APP_VERSION_NO_V}-amd64 \
		-t ${APP_NAME}:${APP_VERSION_NO_V} \
		.

.PHONY: build-image-arm64-with-tags
build-image-arm64-with-tags: go-build-arm64 package-software ## Build Docker image for ARM64 with tags
	@echo "Building mattermost-push-proxy Docker Image for ARM64"
	docker build --no-cache --pull \
		--build-arg TARGETOS=linux \
		--build-arg TARGETARCH=arm64 \
		-f ${DOCKER_FILE} \
		-t ${APP_NAME}:${APP_VERSION_NO_V}-arm64 \
		-t ${APP_NAME}:${APP_VERSION_NO_V} \
		.

.PHONY: docker-build-parallel-with-tags
docker-build-parallel-with-tags: ## Build Docker images for both architectures in parallel
	@echo "Building mattermost-push-proxy Docker Images for both platforms in parallel"
	$(MAKE) build-image-amd64-with-tags &
	$(MAKE) build-image-arm64-with-tags &
	wait
	@echo "Creating multi-platform manifests with clean tags"
	docker manifest create ${APP_NAME}:${APP_VERSION_NO_V} \
		--amend ${APP_NAME}:${APP_VERSION_NO_V}-amd64 \
		--amend ${APP_NAME}:${APP_VERSION_NO_V}-arm64
	@echo "✅ Multi-platform manifests created"

.PHONY: docker-push-with-tags
docker-push-with-tags: ## Push Docker images with unified tags
	@echo "Pushing Docker images to registry"
	docker push ${APP_NAME}:${APP_VERSION_NO_V}-amd64
	docker push ${APP_NAME}:${APP_VERSION_NO_V}-arm64
	docker manifest push ${APP_NAME}:${APP_VERSION_NO_V}
	@echo "Cleaning up intermediate architecture-specific tags from registry"
	docker rmi ${APP_NAME}:${APP_VERSION_NO_V}-amd64
	docker rmi ${APP_NAME}:${APP_VERSION_NO_V}-arm64
	@echo "✅ Multi-platform images pushed with unified tags"
	@echo "✅ Intermediate architecture-specific tags cleaned up from registry"

.PHONY: cleanup-tags
cleanup-tags: ## Clean up intermediate architecture-specific tags from registry
	@echo "Cleaning up intermediate architecture-specific tags from registry"
	@echo "Removing AMD64 tag: ${APP_NAME}:${APP_VERSION_NO_V}-amd64"
	docker rmi ${APP_NAME}:${APP_VERSION_NO_V}-amd64 2>/dev/null || true
	@echo "Removing ARM64 tag: ${APP_NAME}:${APP_VERSION_NO_V}-arm64"
	docker rmi ${APP_NAME}:${APP_VERSION_NO_V}-arm64 2>/dev/null || true
	@echo "✅ Intermediate architecture-specific tags cleaned up from registry"

.PHONY: build-image-fips
build-image-fips: ## Build the FIPS docker image for mattermost-push-proxy
	@echo "Building mattermost-push-proxy FIPS Docker Image for $(TARGET_ARCH)"
	docker build --no-cache --pull \
		--build-arg BUILD_IMAGE=$(BUILD_IMAGE_FIPS) \
		--build-arg BASE_IMAGE=$(BASE_IMAGE_FIPS) \
		--build-arg TARGETOS=$(TARGET_OS) \
		--build-arg TARGETARCH=$(TARGET_ARCH) \
		-f docker/Dockerfile.fips \
		-t $(APP_NAME_FIPS):$(APP_VERSION_NO_V) .

.PHONY: buildx-image-fips
buildx-image-fips: ## Builds and pushes the FIPS docker image for mattermost-push-proxy
	@echo "Building mattermost-push-proxy FIPS Docker Image with buildx"
	docker buildx build --no-cache --pull \
		--platform linux/amd64,linux/arm64 \
		--build-arg BUILD_IMAGE=$(BUILD_IMAGE_FIPS) \
		--build-arg BASE_IMAGE=$(BASE_IMAGE_FIPS) \
		--build-arg TARGETOS=linux \
		--build-arg TARGETARCH=amd64 \
		-f docker/Dockerfile.fips \
		-t $(APP_NAME_FIPS):$(APP_VERSION_NO_V) \
		--push .

## --------------------------------------
## FIPS Multi-Architecture Build Targets (Fast, Parallel)
## --------------------------------------

.PHONY: build-image-fips-amd64-with-tags
build-image-fips-amd64-with-tags: ## Build FIPS Docker image for AMD64 with tags
	@echo "Building mattermost-push-proxy FIPS Docker Image for AMD64"
	docker build --no-cache --pull \
		--build-arg BUILD_IMAGE=$(BUILD_IMAGE_FIPS) \
		--build-arg BASE_IMAGE=$(BASE_IMAGE_FIPS) \
		--build-arg TARGETOS=linux \
		--build-arg TARGETARCH=amd64 \
		-f docker/Dockerfile.fips \
		-t $(APP_NAME_FIPS):$(APP_VERSION_NO_V)-amd64 \
		-t $(APP_NAME_FIPS):$(APP_VERSION_NO_V) \
		.

.PHONY: build-image-fips-arm64-with-tags
build-image-fips-arm64-with-tags: ## Build FIPS Docker image for ARM64 with tags
	@echo "Building mattermost-push-proxy FIPS Docker Image for ARM64"
	docker build --no-cache --pull \
		--build-arg BUILD_IMAGE=$(BUILD_IMAGE_FIPS) \
		--build-arg BASE_IMAGE=$(BASE_IMAGE_FIPS) \
		--build-arg TARGETOS=linux \
		--build-arg TARGETARCH=arm64 \
		-f docker/Dockerfile.fips \
		-t $(APP_NAME_FIPS):$(APP_VERSION_NO_V)-arm64 \
		-t $(APP_NAME_FIPS):$(APP_VERSION_NO_V) \
		.

.PHONY: docker-build-fips-parallel-with-tags
docker-build-fips-parallel-with-tags: ## Build FIPS Docker images for both architectures in parallel (FAST)
	@echo "Building mattermost-push-proxy FIPS Docker Images for both platforms in parallel"
	$(MAKE) build-image-fips-amd64-with-tags &
	$(MAKE) build-image-fips-arm64-with-tags &
	wait
	@echo "Creating multi-platform manifests with clean tags"
	docker manifest create $(APP_NAME_FIPS):$(APP_VERSION_NO_V) \
		--amend $(APP_NAME_FIPS):$(APP_VERSION_NO_V)-amd64 \
		--amend $(APP_NAME_FIPS):$(APP_VERSION_NO_V)-arm64
	@echo "✅ Multi-platform manifests created"

.PHONY: docker-push-fips-with-tags
docker-push-fips-with-tags: ## Push FIPS Docker images with unified tags
	@echo "Pushing FIPS Docker images to registry"
	docker push $(APP_NAME_FIPS):$(APP_VERSION_NO_V)-amd64
	docker push $(APP_NAME_FIPS):$(APP_VERSION_NO_V)-arm64
	docker manifest push $(APP_NAME_FIPS):$(APP_VERSION_NO_V)
	@echo "Cleaning up intermediate architecture-specific tags from registry"
	docker rmi $(APP_NAME_FIPS):$(APP_VERSION_NO_V)-amd64
	docker rmi $(APP_NAME_FIPS):$(APP_VERSION_NO_V)-arm64
	@echo "✅ FIPS multi-platform images pushed with unified tags"
	@echo "✅ Intermediate architecture-specific tags cleaned up from registry"

.PHONY: github-release-fips
github-release-fips: ## Create GitHub release for FIPS version
	@echo "Creating GitHub release for FIPS version"
	gh release create v$(APP_VERSION_FIPS) \
		--title "Release v$(APP_VERSION_FIPS) (FIPS)" \
		--notes "FIPS-compliant release of mattermost-push-proxy" \
		--target main
	@echo "✅ GitHub release created for FIPS version"

.PHONY: cleanup-fips-tags
cleanup-fips-tags: ## Clean up intermediate FIPS architecture-specific tags from registry
	@echo "Cleaning up intermediate FIPS architecture-specific tags from registry"
	@echo "Removing AMD64 tag: $(APP_NAME_FIPS):$(APP_VERSION_NO_V)-amd64"
	docker rmi $(APP_NAME_FIPS):$(APP_VERSION_NO_V)-amd64 2>/dev/null || true
	@echo "Removing ARM64 tag: $(APP_NAME_FIPS):$(APP_VERSION_NO_V)-arm64"
	docker rmi $(APP_NAME_FIPS):$(APP_VERSION_NO_V)-arm64 2>/dev/null || true
	@echo "✅ Intermediate FIPS architecture-specific tags cleaned up from registry"

.PHONY: docker-push
docker-push: ## to push the docker image
	@$(INFO) Pushing to registry...
	$(AT)$(DOCKER) buildx build \
	--no-cache --pull --platform linux/amd64,linux/arm64 \
	-f ${DOCKER_FILE} . \
	-t $(DOCKER_REGISTRY)/${DOCKER_REGISTRY_REPO}:${APP_VERSION_NO_V} $(LATEST_DOCKER_TAG) --push || ${FAIL}
	@$(OK) Pushing to registry $(DOCKER_REGISTRY)/${DOCKER_REGISTRY_REPO}:${APP_VERSION_NO_V}

.PHONY: docker-push-fips
docker-push-fips: ## to push the FIPS docker image (builds for default TARGET_ARCH: $(TARGET_ARCH))
	@$(INFO) Pushing FIPS to registry...
	$(AT)$(DOCKER) build \
	--no-cache --pull \
	--platform $(TARGET_OS)/$(TARGET_ARCH) \
	-f ${DOCKER_FILE}.fips . \
	-t $(DOCKER_REGISTRY)/${DOCKER_REGISTRY_REPO}:${APP_VERSION_NO_V}-fips || ${FAIL}
	$(AT)$(DOCKER) push $(DOCKER_REGISTRY)/${DOCKER_REGISTRY_REPO}:${APP_VERSION_NO_V}-fips || ${FAIL}
	@$(OK) Pushing FIPS to registry $(DOCKER_REGISTRY)/${DOCKER_REGISTRY_REPO}:${APP_VERSION_NO_V}-fips

.PHONY: docker-push-fips-linux-amd64
docker-push-fips-linux-amd64: ## to push the FIPS docker image for Linux AMD64
	@$(INFO) Pushing FIPS Linux AMD64 to registry...
	$(AT)$(DOCKER) build \
	--no-cache --pull \
	--platform linux/amd64 \
	-f ${DOCKER_FILE}.fips . \
	-t $(DOCKER_REGISTRY)/${DOCKER_REGISTRY_REPO}:${APP_VERSION_NO_V}-fips-linux-amd64 || ${FAIL}
	$(AT)$(DOCKER) push $(DOCKER_REGISTRY)/${DOCKER_REGISTRY_REPO}:${APP_VERSION_NO_V}-fips-linux-amd64 || ${FAIL}
	@$(OK) Pushing FIPS Linux AMD64 to registry $(DOCKER_REGISTRY)/${DOCKER_REGISTRY_REPO}:${APP_VERSION_NO_V}-fips-linux-amd64

.PHONY: docker-push-fips-linux-arm64
docker-push-fips-linux-arm64: ## to push the FIPS docker image for Linux ARM64
	@$(INFO) Pushing FIPS Linux ARM64 to registry...
	$(AT)$(DOCKER) build \
	--no-cache --pull \
	--platform linux/arm64 \
	-f ${DOCKER_FILE}.fips . \
	-t $(DOCKER_REGISTRY)/${DOCKER_REGISTRY_REPO}:${APP_VERSION_NO_V}-fips-linux-arm64 || ${FAIL}
	$(AT)$(DOCKER) push $(DOCKER_REGISTRY)/${DOCKER_REGISTRY_REPO}:${APP_VERSION_NO_V}-fips-linux-arm64 || ${FAIL}
	@$(OK) Pushing FIPS Linux ARM64 to registry $(DOCKER_REGISTRY)/${DOCKER_REGISTRY_REPO}:${APP_VERSION_NO_V}-fips-linux-arm64

.PHONY: docker-push-fips-parallel
docker-push-fips-parallel: ## to push FIPS docker images for both architectures in parallel
	@$(INFO) Pushing FIPS Docker images for both architectures in parallel
	$(AT)$(MAKE) docker-push-fips-linux-amd64 &
	$(AT)$(MAKE) docker-push-fips-linux-arm64 &
	wait
	@$(OK) FIPS Docker images pushed for both architectures



.PHONY: docker-sign
docker-sign: ## to sign the docker image
	@$(INFO) Signing the docker image...
	$(AT)echo "$${COSIGN_KEY}" > cosign.key && \
	$(DOCKER) run ${DOCKER_OPTS} \
	--entrypoint '/bin/sh' \
        -v $(PWD):/app -w /app \
	-e COSIGN_PASSWORD=${COSIGN_PASSWORD} \
	-e HOME="/tmp" \
    ${DOCKER_IMAGE_COSIGN} \
	-c \
	"echo Signing... && \
	cosign login $(DOCKER_REGISTRY) -u ${DOCKER_USER} -p ${DOCKER_PASSWORD} && \
	cosign sign --key cosign.key $(DOCKER_REGISTRY)/${DOCKER_REGISTRY_REPO}:${APP_VERSION}" || ${FAIL}
# if we are on a latest semver APP_VERSION tag, also sign latest tag
ifneq ($(shell echo $(APP_VERSION) | egrep '^v([0-9]+\.){0,2}(\*|[0-9]+)'),)
  ifeq ($(shell git tag -l --sort=v:refname | tail -n1),$(APP_VERSION))
	$(DOCKER) run ${DOCKER_OPTS} \
	--entrypoint '/bin/sh' \
        -v $(PWD):/app -w /app \
	-e COSIGN_PASSWORD=${COSIGN_PASSWORD} \
	-e HOME="/tmp" \
	${DOCKER_IMAGE_COSIGN} \
	-c \
	"echo Signing... && \
	cosign login $(DOCKER_REGISTRY) -u ${DOCKER_USER} -p ${DOCKER_PASSWORD} && \
	cosign sign --key cosign.key $(DOCKER_REGISTRY)/${DOCKER_REGISTRY_REPO}:latest" || ${FAIL}
  endif
endif
	$(AT)rm -f cosign.key || ${FAIL}
	@$(OK) Signing the docker image: $(DOCKER_REGISTRY)/${DOCKER_REGISTRY_REPO}:${APP_VERSION}

.PHONY: docker-verify
docker-verify: ## to verify the docker image
	@$(INFO) Verifying the published docker image...
	$(AT)echo "$${COSIGN_PUBLIC_KEY}" > cosign_public.key && \
	$(DOCKER) run ${DOCKER_OPTS} \
	--entrypoint '/bin/sh' \
	-v $(PWD):/app -w /app \
	${DOCKER_IMAGE_COSIGN} \
	-c \
	"echo Verifying... && \
	cosign verify --key cosign_public.key $(DOCKER_REGISTRY)/${DOCKER_REGISTRY_REPO}:${APP_VERSION}" || ${FAIL}
# if we are on a latest semver APP_VERSION tag, also verify latest tag
ifneq ($(shell echo $(APP_VERSION) | egrep '^v([0-9]+\.){0,2}(\*|[0-9]+)'),)
  ifeq ($(shell git tag -l --sort=v:refname | tail -n1),$(APP_VERSION))
	$(DOCKER) run ${DOCKER_OPTS} \
	--entrypoint '/bin/sh' \
	-v $(PWD):/app -w /app \
	${DOCKER_IMAGE_COSIGN} \
	-c \
	"echo Verifying... && \
	cosign verify --key cosign_public.key $(DOCKER_REGISTRY)/${DOCKER_REGISTRY_REPO}:latest" || ${FAIL}
  endif
endif
	$(AT)rm -f cosign_public.key || ${FAIL}
	@$(OK) Verifying the published docker image: $(DOCKER_REGISTRY)/${DOCKER_REGISTRY_REPO}:${APP_VERSION}

.PHONY: docker-sbom
docker-sbom: ## to print a sbom report
	@$(INFO) Performing Docker sbom report...
	$(AT)$(DOCKER) sbom ${APP_NAME}:${APP_VERSION} || ${FAIL}
	@$(OK) Performing Docker sbom report

.PHONY: docker-scan
docker-scan: ## to print a vulnerability report
	@$(INFO) Performing Docker scan report...
	$(AT)$(DOCKER) scan ${APP_NAME}:${APP_VERSION} || ${FAIL}
	@$(OK) Performing Docker scan report

.PHONY: docker-scout
	@$(INFO) Performing Docker scout report...
	$(AT)$(DOCKER) scout cves ${APP_NAME}:${APP_VERSION} || ${FAIL}
	@$(OK) Performing Docker scout report

.PHONY: docker-lint
docker-lint: ## to lint the Dockerfile
	@$(INFO) Dockerfile linting...
	$(AT)$(DOCKER) run -i ${DOCKER_OPTS} \
	${DOCKER_IMAGE_DOCKERLINT} \
	< ${DOCKER_FILE} || ${FAIL}
	@$(OK) Dockerfile linting

.PHONY: docker-login
docker-login: ## to login to a container registry
	@$(INFO) Dockerd login to container registry ${DOCKER_REGISTRY}...
	$(AT) echo "${DOCKER_PASSWORD}" | $(DOCKER) login --password-stdin -u ${DOCKER_USER} $(DOCKER_REGISTRY) || ${FAIL}
	@$(OK) Dockerd login to container registry ${DOCKER_REGISTRY}...

go-build: $(GO_BUILD_PLATFORMS_ARTIFACTS) ## to build binaries

.PHONY: go-build-amd64
go-build-amd64: go-build/$(APP_NAME)-linux-amd64 ## Build AMD64 binary only

.PHONY: go-build-arm64
go-build-arm64: go-build/$(APP_NAME)-linux-arm64 ## Build ARM64 binary only

.PHONY: go-build
go-build/%:
	@$(INFO) go build $*...
	$(AT)target="$*"; \
	command="${APP_NAME}"; \
	platform_ext="$${target#$$command-*}"; \
	platform="$${platform_ext%.*}"; \
	export GOOS="$${platform%%-*}"; \
	export GOARCH="$${platform#*-}"; \
	echo export GOOS=$${GOOS}; \
	echo export GOARCH=$${GOARCH}; \
	CGO_ENABLED=0 \
	$(GO) build ${GO_BUILD_OPTS} \
	-ldflags '${GO_LDFLAGS}' \
	-o ${GO_OUT_BIN_DIR}/$* \
	${CONFIG_APP_CODE} || ${FAIL}
	@$(OK) go build $*

.PHONY: go-build-docker
go-build-docker: # to build binaries under a controlled docker dedicated go container using DOCKER_IMAGE_GO
	@$(INFO) go build docker
	$(AT)$(DOCKER) run  \
	-v $(PWD):/app -w /app \
	$(DOCKER_IMAGE_GO) \
	/bin/sh -c \
	"cd /app && \
	make go-build"  || ${FAIL}
	@$(OK) go build docker

## --------------------------------------
## FIPS Build Targets
## --------------------------------------

_build-fips-internal: ## Internal FIPS build target (used by Dockerfile.fips and build-fips)
	@echo "Building mattermost-push-proxy (FIPS)"
	@mkdir -p $(GO_OUT_BIN_DIR)
	GO111MODULE=on GOOS=$(TARGET_OS) GOARCH=$(TARGET_ARCH) CGO_ENABLED=1 go build -tags=fips,goexperiment.opensslcrypto -trimpath -o $(GO_OUT_BIN_DIR)/mattermost-push-proxy ./main.go

.PHONY: build-fips
build-fips: ## Build the mattermost-push-proxy with FIPS-compliant settings using containerized build
	@echo "Building mattermost-push-proxy (FIPS - $(TARGET_ARCH))"
	docker run --rm -v $(shell pwd):/app -w /app \
		--entrypoint="" \
		-e TARGET_OS=$(TARGET_OS) \
		-e TARGET_ARCH=$(TARGET_ARCH) \
		-e CGO_ENABLED=1 \
		-e GOFIPS=1 \
		-e GOEXPERIMENT=systemcrypto \
		-e HOST_UID=$(shell id -u) \
		-e HOST_GID=$(shell id -g) \
		$(BUILD_IMAGE_FIPS) \
		sh -c "cd /app && make _build-fips-internal TARGET_OS=\$$TARGET_OS TARGET_ARCH=\$$TARGET_ARCH && mv $(GO_OUT_BIN_DIR)/mattermost-push-proxy $(GO_OUT_BIN_DIR)/mattermost-push-proxy-fips-$(TARGET_ARCH) && chown \$$HOST_UID:\$$HOST_GID $(GO_OUT_BIN_DIR)/mattermost-push-proxy-fips-$(TARGET_ARCH)"

.PHONY: build-fips-amd64
build-fips-amd64: ## Build the mattermost-push-proxy with FIPS-compliant settings for AMD64
	$(MAKE) build-fips TARGET_ARCH=amd64

.PHONY: build-fips-arm64
build-fips-arm64: ## Build the mattermost-push-proxy with FIPS-compliant settings for ARM64
	$(MAKE) build-fips TARGET_ARCH=arm64

.PHONY: build-fips-all
build-fips-all: build-fips-amd64 build-fips-arm64 ## Build FIPS binaries for both architectures

.PHONY: go-run
go-run: ## to run locally for development
	@$(INFO) running locally...
	$(AT)$(GO) run ${GO_BUILD_OPTS} ${CONFIG_APP_CODE} || ${FAIL}
	@$(OK) running locally

.PHONY: go-test
go-test: ## to run tests
	@$(INFO) testing...
	$(AT)$(DOCKER) run ${DOCKER_OPTS} \
	-v $(PWD):/app -w /app \
	$(DOCKER_IMAGE_GO) \
	/bin/sh -c \
	"cd /app && \
	go test ${GO_TEST_OPTS} ./... " || ${FAIL}
	@$(OK) testing

.PHONY: go-mod-check
go-mod-check: ## to check go mod files consistency
	@$(INFO) Checking go mod files consistency...
	$(AT)$(GO) mod tidy
	$(AT)git --no-pager diff --exit-code go.mod go.sum || \
	(${WARN} Please run "go mod tidy" and commit the changes in go.mod and go.sum. && ${FAIL} ; exit 128 )
	@$(OK) Checking go mod files consistency

.PHONY: go-lint
go-lint: ## to lint go code
	@$(INFO) App linting...
	$(AT)$(DOCKER) run ${DOCKER_OPTS} \
	-v $(PWD):/app -w /app \
	${DOCKER_IMAGE_GOLINT} \
	golangci-lint run ./... || ${FAIL}
	@$(OK) App linting

.PHONY: go-doc
go-doc: ## to generate documentation
	@$(INFO) Generating Documentation...
	$(AT)$(GO) run ./scripts/env_config.go ./docs/env_config.md || ${FAIL}
	@$(OK) Generating Documentation

.PHONY: check-modules
check-modules: $(OUTDATED_GEN) ## Check outdated modules
	@echo Checking outdated modules
	$(GO) list -mod=mod -u -m -json all | $(OUTDATED_GEN) -update -direct

.PHONY: update-modules
update-modules: ## Update all modules to latest versions
	@echo Updating modules
	$(GO) get -u ./...
	$(GO) mod tidy

.PHONY: scan
scan: ## Scan Docker image for vulnerabilities using Docker Scout
	@echo Running Docker Scout vulnerability scan
	@if ! docker images -q ${APP_NAME}:${APP_VERSION_NO_V} | grep -q .; then \
		echo "❌ Image ${APP_NAME}:${APP_VERSION_NO_V} not found locally. Please build it first with:"; \
		echo "   make build-image-amd64-with-tags (or build-image-arm64-with-tags)"; \
		exit 1; \
	fi
	docker scout cves ${APP_NAME}:${APP_VERSION_NO_V}

.PHONY: scan-fips
scan-fips: ## Scan FIPS Docker image for vulnerabilities using Docker Scout
	@echo Running Docker Scout vulnerability scan for FIPS image
	@if ! docker images -q $(APP_NAME_FIPS):$(APP_VERSION_NO_V) | grep -q .; then \
		echo "❌ Image $(APP_NAME_FIPS):$(APP_VERSION_NO_V) not found locally. Please build it first with:"; \
		echo "   make build-image-fips-amd64-with-tags (or build-image-fips-arm64-with-tags)"; \
		exit 1; \
	fi
	docker scout cves $(APP_NAME_FIPS):$(APP_VERSION_NO_V)

.PHONY: grype
grype: ## Scan Docker image for vulnerabilities using Grype
	@echo Running Grype vulnerability scan
	@if ! docker images -q ${APP_NAME}:${APP_VERSION_NO_V} | grep -q .; then \
		echo "❌ Image ${APP_NAME}:${APP_VERSION_NO_V} not found locally. Please build it first with:"; \
		echo "   make build-image-amd64-with-tags (or build-image-arm64-with-tags)"; \
		exit 1; \
	fi
	grype docker:${APP_NAME}:${APP_VERSION_NO_V} -o table --only-fixed

.PHONY: grype-fips
grype-fips: ## Scan FIPS Docker image for vulnerabilities using Grype
	@echo Running Grype vulnerability scan for FIPS image
	@if ! docker images -q $(APP_NAME_FIPS):$(APP_VERSION_NO_V) | grep -q .; then \
		echo "❌ Image $(APP_NAME_FIPS):$(APP_VERSION_NO_V) not found locally. Please build it first with:"; \
		echo "   make build-image-fips-amd64-with-tags (or build-image-fips-arm64-with-tags)"; \
		exit 1; \
	fi
	grype docker:$(APP_NAME_FIPS):$(APP_VERSION_NO_V) -o table --only-fixed

.PHONY: security-all
security-all: ## Run all vulnerability scans (Docker Scout and Grype) for both regular and FIPS images
	@echo "🔍 Running comprehensive security scans for all images..."
	@echo ""
	@echo "=========================================="
	@echo "📊 Docker Scout - Regular Image"
	@echo "=========================================="
	$(MAKE) scan
	@echo ""
	@echo "=========================================="
	@echo "📊 Docker Scout - FIPS Image"
	@echo "=========================================="
	$(MAKE) scan-fips
	@echo ""
	@echo "=========================================="
	@echo "🛡️  Grype - Regular Image"
	@echo "=========================================="
	$(MAKE) grype
	@echo ""
	@echo "=========================================="
	@echo "🛡️  Grype - FIPS Image"
	@echo "=========================================="
	$(MAKE) grype-fips
	@echo ""
	@echo "✅ All security scans completed!"

.PHONY: security-build-and-scan
security-build-and-scan: ## Build images and run comprehensive security scans
	@echo "🚀 Building images and running comprehensive security scans..."
	@echo ""
	@echo "=========================================="
	@echo "🔨 Building Regular ARM64 Image"
	@echo "=========================================="
	$(MAKE) build-image-arm64-with-tags
	@echo ""
	@echo "=========================================="
	@echo "🔨 Building FIPS ARM64 Image"
	@echo "=========================================="
	$(MAKE) build-image-fips-arm64-with-tags
	@echo ""
	@echo "🔍 Starting security scans..."
	$(MAKE) security-all

.PHONY: github-release
github-release: ## to publish a release and relevant artifacts to GitHub
	@$(INFO) Generating github-release http://github.com/$(GITHUB_ORG)/$(GITHUB_REPO)/releases/tag/$(APP_VERSION) ...
ifeq ($(shell echo $(APP_VERSION) | egrep '^v([0-9]+\.){0,2}(\*|[0-9]+)'),)
	$(error "We only support releases from semver tags")
else
	$(AT)$(DOCKER) run \
	-v $(PWD):/app -w /app \
	-e GITHUB_TOKEN=${GITHUB_TOKEN} \
	$(DOCKER_IMAGE_GH_CLI) \
	/bin/sh -c \
	"git config --global --add safe.directory /app && cd /app && \
	gh release create $(APP_VERSION) --generate-notes $(GO_OUT_BIN_DIR)/*" || ${FAIL}
endif
	@$(OK) Generating github-release http://github.com/$(GITHUB_ORG)/$(GITHUB_REPO)/releases/tag/$(APP_VERSION) ...



.PHONY: github-release-all
github-release-all: ## to publish both normal and FIPS releases to GitHub
	@$(INFO) Generating both releases...
	$(AT)$(MAKE) github-release
	$(AT)$(MAKE) github-release-fips
	@$(OK) Generating both releases

.PHONY: clean
clean: ## to clean-up
	@$(INFO) cleaning /${GO_OUT_BIN_DIR} folder...
	$(AT)rm -rf ${GO_OUT_BIN_DIR} || ${FAIL}
	@$(OK) cleaning /${GO_OUT_BIN_DIR} folder


## --------------------------------------
## Tooling Binaries
## --------------------------------------
$(OUTDATED_GEN): ## Build go-mod-outdated.
	GOBIN=$(TOOLS_BIN_DIR) $(GO_INSTALL) github.com/psampaz/go-mod-outdated $(OUTDATED_BIN) $(OUTDATED_VER)


================================================
FILE: NOTICE.txt
================================================
Mattermost Platform 
© 2015 Mattermost, Inc.  All Rights Reserved.  See LICENSE.txt for license information.

NOTICES: 
--------


================================================
FILE: README.md
================================================
# Mattermost Push Proxy ![CircleCI branch](https://img.shields.io/circleci/project/github/mattermost/mattermost-push-proxy/master.svg)

See https://developers.mattermost.com/contribute/mobile/push-notifications/service/


# How to Release

To trigger a release of Mattermost Push-Proxy, follow these steps:

1. **For Patch Release:** Run the following command:
    ```
    make patch
    ```
   This will release a patch change.

2. **For Minor Release:** Run the following command:
    ```
    make minor
    ```
   This will release a minor change.

3. **For Major Release:** Run the following command:
    ```
    make major
    ```
   This will release a major change.


================================================
FILE: bin/.gitignore
================================================
# Ignore all files in this directory
*
# Except this one
!.gitignore


================================================
FILE: build/setup.txt
================================================
#!/bin/bash

# copy following lines to /etc/init/matter-push-proxy.conf

# start on runlevel [2345]
# stop on runlevel [016]
# respawn
# chdir /home/ubuntu/matter-push-proxy
# setuid ubuntu
# console output
# exec bin/push-proxy | logger

sudo stop matter-push-proxy


rm -f matter-push-proxy.tar.gz
rm -rf ~/matter-push-proxy

wget https://github.com/mattermost/push-proxy/releases/download/v0.1.0/matter-push-proxy.tar.gz

mkdir -p ~/matter-push-proxy

tar -C ~/ -xzf matter-push-proxy.tar.gz

cp config-push-proxy.json ~/matter-push-proxy/config/config-push-proxy.json

sudo start matter-push-proxy

sleep 10

curl localhost:8066

================================================
FILE: config/mattermost-push-proxy.sample.json
================================================
{
    "ListenAddress":":8066",
    "ThrottlePerSec":300,
    "ThrottleMemoryStoreSize":50000,
    "ThrottleVaryByHeader":"X-Forwarded-For",
    "EnableMetrics": false,
    "SendTimeoutSec": 30,
    "RetryTimeoutSec": 8,
    "ApplePushSettings":[
        {
            "Type":"apple",
            "ApplePushUseDevelopment":false,
            "ApplePushCertPrivate":"",
            "ApplePushCertPassword":"",
            "ApplePushTopic":"com.mattermost.Mattermost",
            "AppleAuthKeyFile": "",
            "AppleAuthKeyID": "",
            "AppleTeamID": ""
        },
        {
            "Type":"apple_rn",
            "ApplePushUseDevelopment":false,
            "ApplePushCertPrivate":"",
            "ApplePushCertPassword":"",
            "ApplePushTopic":"com.mattermost.react.native",
            "AppleAuthKeyFile": "",
            "AppleAuthKeyID": "",
            "AppleTeamID": ""
        }
    ],
    "AndroidPushSettings": [
        {
            "Type":"android",
            "ServiceFileLocation":""
        },
        {
            "Type":"android_rn",
            "ServiceFileLocation":""
        }
    ],
    "EnableConsoleLog": true,
    "EnableFileLog": false,
    "LogFileLocation": "",
    "LogFormat": "plain"
}


================================================
FILE: docker/Dockerfile
================================================
FROM --platform=${TARGETPLATFORM} ubuntu:noble-20250529 AS builder
ARG TARGETARCH

# Setting bash as our shell, and enabling pipefail option
SHELL ["/bin/bash", "-o", "pipefail", "-c"]

# Copying tarball
WORKDIR /mattermost-push-proxy
COPY dist/mattermost-push-proxy-linux-${TARGETARCH}.tar.gz /mattermost-push-proxy-linux.tar.gz
RUN tar -xf /mattermost-push-proxy-linux.tar.gz --strip-components=1  -C /mattermost-push-proxy

FROM --platform=${TARGETPLATFORM} ubuntu:noble-20250529

# Install needed packages and indirect dependencies
# hadolint ignore=DL3008
RUN apt-get update \
  && DEBIAN_FRONTEND=noninteractive apt-get install --no-install-recommends -y \
  ca-certificates \
  libffi-dev \
  netcat-openbsd \
  tzdata \
  && rm -rf /var/lib/apt/lists/*

# Coyping needed files from previous stage
COPY --from=builder /mattermost-push-proxy/ /mattermost-push-proxy/
COPY docker/entrypoint /usr/local/bin/

RUN chown -R nobody:nogroup /mattermost-push-proxy

USER nobody
WORKDIR /mattermost-push-proxy
ENV PUSH_PROXY=/mattermost-push-proxy/bin/mattermost-push-proxy

EXPOSE 8066
VOLUME ["/mattermost-push-proxy/config", "/mattermost-push-proxy/certs"]

ENTRYPOINT ["/usr/local/bin/entrypoint"]


================================================
FILE: docker/Dockerfile.fips
================================================
# Build the mattermost-push-proxy (FIPS version)
ARG BUILD_IMAGE=cgr.dev/mattermost.com/go-msft-fips:1.24.6
ARG BASE_IMAGE=cgr.dev/mattermost.com/glibc-openssl-fips:15.2

FROM ${BUILD_IMAGE} AS builder

ARG TARGETARCH
ARG TARGETOS

WORKDIR /workspace
COPY . .

# Copy config file to workspace for later use
COPY config/mattermost-push-proxy.sample.json /workspace/mattermost-push-proxy.json

# Build with FIPS-compliant settings
ENV CGO_ENABLED=1
ENV GOFIPS=1
ENV GOEXPERIMENT=systemcrypto
RUN make _build-fips-internal TARGET_OS=$TARGETOS TARGET_ARCH=$TARGETARCH

FROM ${BASE_IMAGE}

LABEL name="Mattermost Push Proxy (FIPS)" \
  maintainer="sre@mattermost.com" \
  vendor="Mattermost" \
  distribution-scope="public" \
  url="https://mattermost.dev" \
  io.k8s.description="Mattermost Push Proxy service for FIPS-compliant environments" \
  io.k8s.display-name="Mattermost Push Proxy (FIPS)" \
  io.openshift.tags="mattermost,push-proxy,fips" \
  summary="FIPS-compliant Mattermost Push Proxy service" \
  description="Mattermost Push Proxy service for FIPS-compliant environments. This is a FIPS-compliant version." \
  com.mattermost.fips="true" \
  com.mattermost.fips.version="140-2"

WORKDIR /mattermost-push-proxy

    # Copy files - directories will be created automatically
    COPY --from=builder /workspace/dist/mattermost-push-proxy /mattermost-push-proxy/bin/mattermost-push-proxy
    COPY --from=builder /workspace/mattermost-push-proxy.json /mattermost-push-proxy/config/mattermost-push-proxy.json

USER 65532

EXPOSE 8066
VOLUME ["/mattermost-push-proxy/config", "/mattermost-push-proxy/certs"]

# Use the binary from bin directory
ENTRYPOINT ["/mattermost-push-proxy/bin/mattermost-push-proxy"]


================================================
FILE: docker/entrypoint
================================================
#!/bin/sh -e

exec ${PUSH_PROXY} $@


================================================
FILE: go.mod
================================================
module github.com/mattermost/mattermost-push-proxy

go 1.24.6

require (
	firebase.google.com/go/v4 v4.18.0
	github.com/gorilla/handlers v1.5.2
	github.com/gorilla/mux v1.8.1
	github.com/kyokomi/emoji v2.2.4+incompatible
	github.com/mattermost/mattermost/server/public v0.1.16
	github.com/prometheus/client_golang v1.23.0
	github.com/prometheus/common v0.65.0
	github.com/sideshow/apns2 v0.25.0
	github.com/stretchr/testify v1.10.0
	golang.org/x/net v0.43.0
	golang.org/x/oauth2 v0.30.0
	google.golang.org/api v0.248.0
	gopkg.in/throttled/throttled.v1 v1.0.0
)

require (
	cel.dev/expr v0.24.0 // indirect
	cloud.google.com/go v0.121.6 // indirect
	cloud.google.com/go/auth v0.16.5 // indirect
	cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
	cloud.google.com/go/compute/metadata v0.8.0 // indirect
	cloud.google.com/go/firestore v1.18.0 // indirect
	cloud.google.com/go/iam v1.5.2 // indirect
	cloud.google.com/go/longrunning v0.6.7 // indirect
	cloud.google.com/go/monitoring v1.24.2 // indirect
	cloud.google.com/go/storage v1.56.1 // indirect
	github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0 // indirect
	github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.53.0 // indirect
	github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.53.0 // indirect
	github.com/MicahParks/keyfunc v1.9.0 // indirect
	github.com/PuerkitoBio/boom v0.0.0-20140219125548-fecdef1c97ca // indirect
	github.com/beorn7/perks v1.0.1 // indirect
	github.com/cespare/xxhash/v2 v2.3.0 // indirect
	github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443 // indirect
	github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
	github.com/envoyproxy/go-control-plane/envoy v1.32.4 // indirect
	github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect
	github.com/felixge/httpsnoop v1.0.4 // indirect
	github.com/francoispqt/gojay v1.2.13 // indirect
	github.com/garyburd/redigo v1.6.4 // indirect
	github.com/go-jose/go-jose/v4 v4.1.2 // indirect
	github.com/go-logr/logr v1.4.3 // indirect
	github.com/go-logr/stdr v1.2.2 // indirect
	github.com/golang-jwt/jwt/v4 v4.5.2 // indirect
	github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
	github.com/golang/protobuf v1.5.4 // indirect
	github.com/google/s2a-go v0.1.9 // indirect
	github.com/google/uuid v1.6.0 // indirect
	github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect
	github.com/googleapis/gax-go/v2 v2.15.0 // indirect
	github.com/mattermost/logr/v2 v2.0.22 // indirect
	github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
	github.com/olekukonko/ts v0.0.0-20171002115256-78ecb04241c0 // indirect
	github.com/pkg/errors v0.9.1 // indirect
	github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
	github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
	github.com/prometheus/client_model v0.6.2 // indirect
	github.com/prometheus/procfs v0.17.0 // indirect
	github.com/rakyll/pb v0.0.0-20160123035540-8d46b8b097ef // indirect
	github.com/spiffe/go-spiffe/v2 v2.5.0 // indirect
	github.com/wiggin77/merror v1.0.5 // indirect
	github.com/wiggin77/srslog v1.0.1 // indirect
	github.com/zeebo/errs v1.4.0 // indirect
	go.opentelemetry.io/auto/sdk v1.1.0 // indirect
	go.opentelemetry.io/contrib/detectors/gcp v1.37.0 // indirect
	go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.62.0 // indirect
	go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0 // indirect
	go.opentelemetry.io/otel v1.37.0 // indirect
	go.opentelemetry.io/otel/metric v1.37.0 // indirect
	go.opentelemetry.io/otel/sdk v1.37.0 // indirect
	go.opentelemetry.io/otel/sdk/metric v1.37.0 // indirect
	go.opentelemetry.io/otel/trace v1.37.0 // indirect
	golang.org/x/crypto v0.41.0 // indirect
	golang.org/x/sync v0.16.0 // indirect
	golang.org/x/sys v0.35.0 // indirect
	golang.org/x/text v0.28.0 // indirect
	golang.org/x/time v0.12.0 // indirect
	google.golang.org/appengine/v2 v2.0.6 // indirect
	google.golang.org/genproto v0.0.0-20250818200422-3122310a409c // indirect
	google.golang.org/genproto/googleapis/api v0.0.0-20250818200422-3122310a409c // indirect
	google.golang.org/genproto/googleapis/rpc v0.0.0-20250818200422-3122310a409c // indirect
	google.golang.org/grpc v1.75.0 // indirect
	google.golang.org/protobuf v1.36.7 // indirect
	gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
	gopkg.in/yaml.v3 v3.0.1 // indirect
)


================================================
FILE: go.sum
================================================
cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY=
cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw=
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.31.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.37.0/go.mod h1:TS1dMSSfndXH133OKGwekG838Om/cQT0BUHV3HcBgoo=
cloud.google.com/go v0.121.6 h1:waZiuajrI28iAf40cWgycWNgaXPO06dupuS+sgibK6c=
cloud.google.com/go v0.121.6/go.mod h1:coChdst4Ea5vUpiALcYKXEpR1S9ZgXbhEzzMcMR66vI=
cloud.google.com/go/auth v0.16.5 h1:mFWNQ2FEVWAliEQWpAdH80omXFokmrnbDhUS9cBywsI=
cloud.google.com/go/auth v0.16.5/go.mod h1:utzRfHMP+Vv0mpOkTRQoWD2q3BatTOoWbA7gCc2dUhQ=
cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
cloud.google.com/go/compute/metadata v0.8.0 h1:HxMRIbao8w17ZX6wBnjhcDkW6lTFpgcaobyVfZWqRLA=
cloud.google.com/go/compute/metadata v0.8.0/go.mod h1:sYOGTp851OV9bOFJ9CH7elVvyzopvWQFNNghtDQ/Biw=
cloud.google.com/go/firestore v1.18.0 h1:cuydCaLS7Vl2SatAeivXyhbhDEIR8BDmtn4egDhIn2s=
cloud.google.com/go/firestore v1.18.0/go.mod h1:5ye0v48PhseZBdcl0qbl3uttu7FIEwEYVaWm0UIEOEU=
cloud.google.com/go/iam v1.5.2 h1:qgFRAGEmd8z6dJ/qyEchAuL9jpswyODjA2lS+w234g8=
cloud.google.com/go/iam v1.5.2/go.mod h1:SE1vg0N81zQqLzQEwxL2WI6yhetBdbNQuTvIKCSkUHE=
cloud.google.com/go/logging v1.13.0 h1:7j0HgAp0B94o1YRDqiqm26w4q1rDMH7XNRU34lJXHYc=
cloud.google.com/go/logging v1.13.0/go.mod h1:36CoKh6KA/M0PbhPKMq6/qety2DCAErbhXT62TuXALA=
cloud.google.com/go/longrunning v0.6.7 h1:IGtfDWHhQCgCjwQjV9iiLnUta9LBCo8R9QmAFsS/PrE=
cloud.google.com/go/longrunning v0.6.7/go.mod h1:EAFV3IZAKmM56TyiE6VAP3VoTzhZzySwI/YI1s/nRsY=
cloud.google.com/go/monitoring v1.24.2 h1:5OTsoJ1dXYIiMiuL+sYscLc9BumrL3CarVLL7dd7lHM=
cloud.google.com/go/monitoring v1.24.2/go.mod h1:x7yzPWcgDRnPEv3sI+jJGBkwl5qINf+6qY4eq0I9B4U=
cloud.google.com/go/storage v1.56.1 h1:n6gy+yLnHn0hTwBFzNn8zJ1kqWfR91wzdM8hjRF4wP0=
cloud.google.com/go/storage v1.56.1/go.mod h1:C9xuCZgFl3buo2HZU/1FncgvvOgTAs/rnh4gF4lMg0s=
cloud.google.com/go/trace v1.11.6 h1:2O2zjPzqPYAHrn3OKl029qlqG6W8ZdYaOWRyr8NgMT4=
cloud.google.com/go/trace v1.11.6/go.mod h1:GA855OeDEBiBMzcckLPE2kDunIpC72N+Pq8WFieFjnI=
dmitri.shuralyov.com/app/changes v0.0.0-20180602232624-0a106ad413e3/go.mod h1:Yl+fi1br7+Rr3LqpNJf1/uxUdtRUV+Tnj0o93V2B9MU=
dmitri.shuralyov.com/html/belt v0.0.0-20180602232347-f7d459c86be0/go.mod h1:JLBrvjyP0v+ecvNYvCpyZgu5/xkfAUhi6wJj28eUfSU=
dmitri.shuralyov.com/service/change v0.0.0-20181023043359-a85b471d5412/go.mod h1:a1inKt/atXimZ4Mv927x+r7UpyzRUf4emIoiiSC2TN4=
dmitri.shuralyov.com/state v0.0.0-20180228185332-28bcc343414c/go.mod h1:0PRwlb0D6DFvNNtx+9ybjezNCa8XF0xaYcETyp6rHWU=
firebase.google.com/go/v4 v4.18.0 h1:S+g0P72oDGqOaG4wlLErX3zQmU9plVdu7j+Bc3R1qFw=
firebase.google.com/go/v4 v4.18.0/go.mod h1:P7UfBpzc8+Z3MckX79+zsWzKVfpGryr6HLbAe7gCWfs=
git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0 h1:UQUsRi8WTzhZntp5313l+CHIAT95ojUI2lpP/ExlZa4=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0/go.mod h1:Cz6ft6Dkn3Et6l2v2a9/RpN7epQ1GtDlO6lj8bEcOvw=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.53.0 h1:owcC2UnmsZycprQ5RfRgjydWhuoxg71LUfyiQdijZuM=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.53.0/go.mod h1:ZPpqegjbE99EPKsu3iUWV22A04wzGPcAY/ziSIQEEgs=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.53.0 h1:4LP6hvB4I5ouTbGgWtixJhgED6xdf67twf9PoY96Tbg=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.53.0/go.mod h1:jUZ5LYlw40WMd07qxcQJD5M40aUxrfwqQX1g7zxYnrQ=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.53.0 h1:Ron4zCA/yk6U7WOBXhTJcDpsUBG9npumK6xw2auFltQ=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.53.0/go.mod h1:cSgYe11MCNYunTnRXrKiR/tHc0eoKjICUuWpNZoVCOo=
github.com/MicahParks/keyfunc v1.9.0 h1:lhKd5xrFHLNOWrDc4Tyb/Q1AJ4LCzQ48GVJyVIID3+o=
github.com/MicahParks/keyfunc v1.9.0/go.mod h1:IdnCilugA0O/99dW+/MkvlyrsX8+L8+x95xuVNtM5jw=
github.com/PuerkitoBio/boom v0.0.0-20140219125548-fecdef1c97ca h1:jv7AlMqwTYg92zzES80+2pXD0bPY5kGT3AhFKdXLLdI=
github.com/PuerkitoBio/boom v0.0.0-20140219125548-fecdef1c97ca/go.mod h1:BUNf81ELJpN4dRN2LS5r1fkTSLkczJubIXjJv04ib70=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20201120081800-1786d5ef83d4/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE=
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g=
github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443 h1:aQ3y1lwWyqYPiWZThqv1aFbZMiM9vblcSArJRf2Irls=
github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8=
github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/envoyproxy/go-control-plane v0.13.4 h1:zEqyPVyku6IvWCFwux4x9RxkLOMUL+1vC9xUFv5l2/M=
github.com/envoyproxy/go-control-plane v0.13.4/go.mod h1:kDfuBlDVsSj2MjrLEtRWtHlsWIFcGyB2RMO44Dc5GZA=
github.com/envoyproxy/go-control-plane/envoy v1.32.4 h1:jb83lalDRZSpPWW2Z7Mck/8kXZ5CQAFYVjQcdVIr83A=
github.com/envoyproxy/go-control-plane/envoy v1.32.4/go.mod h1:Gzjc5k8JcJswLjAx1Zm+wSYE20UrLtt7JZMWiWQXQEw=
github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI=
github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4=
github.com/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfUKS7KJ7spH3d86P8=
github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
github.com/francoispqt/gojay v1.2.13 h1:d2m3sFjloqoIUQU3TsHBgj6qg/BVGlTBeHDUmyJnXKk=
github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/garyburd/redigo v1.6.4 h1:LFu2R3+ZOPgSMWMOL+saa/zXRjw0ID2G8FepO53BGlg=
github.com/garyburd/redigo v1.6.4/go.mod h1:rTb6epsqigu3kYKBnaF028A7Tf/Aw5s0cqA47doKKqw=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
github.com/go-jose/go-jose/v4 v4.1.2 h1:TK/7NqRQZfgAh+Td8AlsrvtPoUyiHh0LqVvokh+1vHI=
github.com/go-jose/go-jose/v4 v4.1.2/go.mod h1:22cg9HWM1pOlnRiY+9cQYJ9XHmya1bYW8OeDM6Ku6Oo=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/golang-jwt/jwt/v4 v4.4.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang-jwt/jwt/v4 v4.4.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=
github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ=
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/martian/v3 v3.3.3 h1:DIhPTQrbPkgs2yJYdXU/eNACCG5DVQjySNRNlflZ9Fc=
github.com/google/martian/v3 v3.3.3/go.mod h1:iEPrYcgCF7jA9OtScMFQyAlZZ4YXTKEtJ1E6RWzmBA0=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4=
github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA=
github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY=
github.com/googleapis/gax-go/v2 v2.0.3/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE08qbEPm1M08qg=
github.com/googleapis/gax-go/v2 v2.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81vgd/bo=
github.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE=
github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w=
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw=
github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/kyokomi/emoji v2.2.4+incompatible h1:np0woGKwx9LiHAQmwZx79Oc0rHpNw3o+3evou4BEPv4=
github.com/kyokomi/emoji v2.2.4+incompatible/go.mod h1:mZ6aGCD7yk8j6QY6KICwnZ2pxoszVseX1DNoGtU2tBA=
github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI=
github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mattermost/logr/v2 v2.0.22 h1:npFkXlkAWR9J8payh8ftPcCZvLbHSI125mAM5/r/lP4=
github.com/mattermost/logr/v2 v2.0.22/go.mod h1:0sUKpO+XNMZApeumaid7PYaUZPBIydfuWZ0dqixXo+s=
github.com/mattermost/mattermost/server/public v0.1.16 h1:CtTzNnuFCNndUaNfQse5NnvzfBZt+8P+wOU7OMn07zg=
github.com/mattermost/mattermost/server/public v0.1.16/go.mod h1:hvxMXqfao9JDHM3auk8MLl7DD6jAuG0q27Kf9y6z1r0=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo=
github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM=
github.com/olekukonko/ts v0.0.0-20171002115256-78ecb04241c0 h1:LiZB1h0GIcudcDci2bxbqI6DXV8bF8POAnArqvRrIyw=
github.com/olekukonko/ts v0.0.0-20171002115256-78ecb04241c0/go.mod h1:F/7q8/HZz+TXjlsoZQQKVYvXTZaFH4QRa3y+j1p7MS0=
github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo=
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v1.23.0 h1:ust4zpdl9r4trLY/gSjlm07PuiBq2ynaXXlptpfy8Uc=
github.com/prometheus/client_golang v1.23.0/go.mod h1:i/o0R9ByOnHX0McrTMTyhYvKE4haaf2mW08I+jGAjEE=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE=
github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0=
github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw=
github.com/rakyll/pb v0.0.0-20160123035540-8d46b8b097ef h1:EzrUZp6kh2we8o30Qhx5SJxg7jaiVwlq3WaecTc4BOo=
github.com/rakyll/pb v0.0.0-20160123035540-8d46b8b097ef/go.mod h1:6Wyuoi/kwXz73J3mwjVSCvIr9QprJ7Q9GrogPi1QZ+8=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/shurcooL/component v0.0.0-20170202220835-f88ec8f54cc4/go.mod h1:XhFIlyj5a1fBNx5aJTbKoIq0mNaPvOagO+HjB3EtxrY=
github.com/shurcooL/events v0.0.0-20181021180414-410e4ca65f48/go.mod h1:5u70Mqkb5O5cxEA8nxTsgrgLehJeAw6Oc4Ab1c/P1HM=
github.com/shurcooL/github_flavored_markdown v0.0.0-20181002035957-2122de532470/go.mod h1:2dOwnU2uBioM+SGy2aZoq1f/Sd1l9OkAeAUvjSyvgU0=
github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk=
github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ=
github.com/shurcooL/gofontwoff v0.0.0-20180329035133-29b52fc0a18d/go.mod h1:05UtEgK5zq39gLST6uB0cf3NEHjETfB4Fgr3Gx5R9Vw=
github.com/shurcooL/gopherjslib v0.0.0-20160914041154-feb6d3990c2c/go.mod h1:8d3azKNyqcHP1GaQE/c6dDgjkgSx2BZ4IoEi4F1reUI=
github.com/shurcooL/highlight_diff v0.0.0-20170515013008-09bb4053de1b/go.mod h1:ZpfEhSmds4ytuByIcDnOLkTHGUI6KNqRNPDLHDk+mUU=
github.com/shurcooL/highlight_go v0.0.0-20181028180052-98c3abbbae20/go.mod h1:UDKB5a1T23gOMUJrI+uSuH0VRDStOiUVSjBTRDVBVag=
github.com/shurcooL/home v0.0.0-20181020052607-80b7ffcb30f9/go.mod h1:+rgNQw2P9ARFAs37qieuu7ohDNQ3gds9msbT2yn85sg=
github.com/shurcooL/htmlg v0.0.0-20170918183704-d01228ac9e50/go.mod h1:zPn1wHpTIePGnXSHpsVPWEktKXHr6+SS6x/IKRb7cpw=
github.com/shurcooL/httperror v0.0.0-20170206035902-86b7830d14cc/go.mod h1:aYMfkZ6DWSJPJ6c4Wwz3QtW22G7mf/PEgaB9k/ik5+Y=
github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg=
github.com/shurcooL/httpgzip v0.0.0-20180522190206-b1c53ac65af9/go.mod h1:919LwcH0M7/W4fcZ0/jy0qGght1GIhqyS/EgWGH2j5Q=
github.com/shurcooL/issues v0.0.0-20181008053335-6292fdc1e191/go.mod h1:e2qWDig5bLteJ4fwvDAc2NHzqFEthkqn7aOZAOpj+PQ=
github.com/shurcooL/issuesapp v0.0.0-20180602232740-048589ce2241/go.mod h1:NPpHK2TI7iSaM0buivtFUc9offApnI0Alt/K8hcHy0I=
github.com/shurcooL/notifications v0.0.0-20181007000457-627ab5aea122/go.mod h1:b5uSkrEVM1jQUspwbixRBhaIjIzL2xazXp6kntxYle0=
github.com/shurcooL/octicon v0.0.0-20181028054416-fa4f57f9efb2/go.mod h1:eWdoE5JD4R5UVWDucdOPg1g2fqQRq78IQa9zlOV1vpQ=
github.com/shurcooL/reactions v0.0.0-20181006231557-f2e0b4ca5b82/go.mod h1:TCR1lToEk4d2s07G3XGfz2QrgHXg4RJBvjrOozvoWfk=
github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/shurcooL/users v0.0.0-20180125191416-49c67e49c537/go.mod h1:QJTqeLYEDaXHZDBsXlPCDqdhQuJkuw4NOtaxYe3xii4=
github.com/shurcooL/webdavfs v0.0.0-20170829043945-18c3829fa133/go.mod h1:hKmq5kWdCj2z2KEozexVbfEZIWiTjhE0+UjmZgPqehw=
github.com/sideshow/apns2 v0.25.0 h1:XOzanncO9MQxkb03T/2uU2KcdVjYiIf0TMLzec0FTW4=
github.com/sideshow/apns2 v0.25.0/go.mod h1:7Fceu+sL0XscxrfLSkAoH6UtvKefq3Kq1n4W3ayQZqE=
github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE=
github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA=
github.com/spiffe/go-spiffe/v2 v2.5.0 h1:N2I01KCUkv1FAjZXJMwh95KK1ZIQLYbPfhaxw8WS0hE=
github.com/spiffe/go-spiffe/v2 v2.5.0/go.mod h1:P+NxobPc6wXhVtINNtFjNWGBTreew1GBUCwT2wPmb7g=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA=
github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU=
github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM=
github.com/wiggin77/merror v1.0.5 h1:P+lzicsn4vPMycAf2mFf7Zk6G9eco5N+jB1qJ2XW3ME=
github.com/wiggin77/merror v1.0.5/go.mod h1:H2ETSu7/bPE0Ymf4bEwdUoo73OOEkdClnoRisfw0Nm0=
github.com/wiggin77/srslog v1.0.1 h1:gA2XjSMy3DrRdX9UqLuDtuVAAshb8bE1NhX1YK0Qe+8=
github.com/wiggin77/srslog v1.0.1/go.mod h1:fehkyYDq1QfuYn60TDPu9YdY2bB85VUW2mvN1WynEls=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/zeebo/errs v1.4.0 h1:XNdoD/RRMKP7HD0UhJnIzUy74ISdGGxURlYG8HSWSfM=
github.com/zeebo/errs v1.4.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4=
go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/contrib/detectors/gcp v1.37.0 h1:B+WbN9RPsvobe6q4vP6KgM8/9plR/HNjgGBrfcOlweA=
go.opentelemetry.io/contrib/detectors/gcp v1.37.0/go.mod h1:K5zQ3TT7p2ru9Qkzk0bKtCql0RGkPj9pRjpXgZJZ+rU=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.62.0 h1:rbRJ8BBoVMsQShESYZ0FkvcITu8X8QNwJogcLUmDNNw=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.62.0/go.mod h1:ru6KHrNtNHxM4nD/vd6QrLVWgKhxPYgblq4VAtNawTQ=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0 h1:Hf9xI/XLML9ElpiHVDNwvqI0hIFlzV8dgIr35kV1kRU=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0/go.mod h1:NfchwuyNoMcZ5MLHwPrODwUF1HWCXWrL31s8gSAdIKY=
go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.36.0 h1:rixTyDGXFxRy1xzhKrotaHy3/KXdPhlWARrCgK+eqUY=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.36.0/go.mod h1:dowW6UsM9MKbJq5JTz2AMVp3/5iW5I/TStsk8S+CfHw=
go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI=
go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg=
go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc=
go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps=
go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE=
golang.org/x/build v0.0.0-20190111050920-041ab4dc3f9d/go.mod h1:OWs+y06UdEOHN4y+MfF/py+xQ/tYqIWW03b70/CG9Rw=
golang.org/x/crypto v0.0.0-20170512130425-ab89591268e0/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181029044818-c44066c5c816/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190313220215-9f648a60d977/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220403103023-749bd193bc2b/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
golang.org/x/perf v0.0.0-20180704124530-6e6d33e29852/go.mod h1:JLpeXjPJfIyPr5TlbXLkXWLhP8nz10XfvxElABhCtcw=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181029174526-d69651ed3497/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190316082340-a2f829d7f35f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20181030000716-a0a13e073c7b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
google.golang.org/api v0.0.0-20181030000543-1d582fd0359e/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
google.golang.org/api v0.1.0/go.mod h1:UGEZY7KEX120AnNLIHFMKIo4obdJhkp2tPbaPlQx13Y=
google.golang.org/api v0.248.0 h1:hUotakSkcwGdYUqzCRc5yGYsg4wXxpkKlW5ryVqvC1Y=
google.golang.org/api v0.248.0/go.mod h1:yAFUAF56Li7IuIQbTFoLwXTCI6XCFKueOlS7S9e4F9k=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine/v2 v2.0.6 h1:LvPZLGuchSBslPBp+LAhihBeGSiRh1myRoYK4NtuBIw=
google.golang.org/appengine/v2 v2.0.6/go.mod h1:WoEXGoXNfa0mLvaH5sV3ZSGXwVmy8yf7Z1JKf3J3wLI=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg=
google.golang.org/genproto v0.0.0-20190306203927-b5d61aea6440/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20250818200422-3122310a409c h1:ZERoum3uuqL0PRSc6SXielu26FN96T4BUGaaW0oL+c8=
google.golang.org/genproto v0.0.0-20250818200422-3122310a409c/go.mod h1:Q8kep885BJnK3Jt6QZXIFeLHSzoAQtlI1CCloQigiyU=
google.golang.org/genproto/googleapis/api v0.0.0-20250818200422-3122310a409c h1:AtEkQdl5b6zsybXcbz00j1LwNodDuH6hVifIaNqk7NQ=
google.golang.org/genproto/googleapis/api v0.0.0-20250818200422-3122310a409c/go.mod h1:ea2MjsO70ssTfCjiwHgI0ZFqcw45Ksuk2ckf9G468GA=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250818200422-3122310a409c h1:qXWI/sQtv5UKboZ/zUk7h+mrf/lXORyI+n9DKDAusdg=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250818200422-3122310a409c/go.mod h1:gw1tLEfykwDz2ET4a12jcXt4couGAm7IwsVaTy0Sflo=
google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio=
google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.75.0 h1:+TW+dqTd2Biwe6KKfhE5JpiYIBWq865PhKGSXiivqt4=
google.golang.org/grpc v1.75.0/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.36.7 h1:IgrO7UwFQGJdRNXH/sQux4R1Dj1WAKcLElzeeRaXV2A=
google.golang.org/protobuf v1.36.7/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
gopkg.in/throttled/throttled.v1 v1.0.0 h1:HW4VuZPcA2x88dJSf3T7GLTOwYCrdQcYDEC65ZEX2mQ=
gopkg.in/throttled/throttled.v1 v1.0.0/go.mod h1:UIVpydfpoUKqbHErIHUoEnuOj9KVPAmS88/iAIqBScE=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o=
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
sourcegraph.com/sourcegraph/go-diff v0.5.0/go.mod h1:kuch7UrkMzY0X+p9CRK03kfuPQ2zzQcaEFbx8wA8rck=
sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0=


================================================
FILE: internal/version/version.go
================================================
package version

import (
	"fmt"
	"runtime"
	"strings"
	"text/tabwriter"
)

// Base version information.
//
// This is the fallback data used when version information from git is not
// provided via go ldflags (e.g. via Makefile).
var (
	// Output of "git describe". The prerequisite is that the branch should be
	// tagged using the correct versioning strategy.
	gitVersion string = "devel"
	// short SHA1 from git, output of $(git rev-parse --short HEAD)
	buildHash = "unknown"
	// the most recent v* tag in the current branch (or its ancestors)
	buildTagLatest = "unknown"
	// the current commit's v* tag
	buildTagCurrent = "unknown"
	// State of git tree, either "clean" or "dirty"
	gitTreeState = "unknown"
	// Build date in ISO8601 format, output of $(date -u +'%Y-%m-%dT%H:%M:%SZ')
	buildDate = "unknown"
)

func GetVersion() error {
	v := VersionInfo()
	res := v.String()

	fmt.Println(res)
	return nil
}

type Info struct {
	GitVersion   string `json:"-"`
	BuildHash    string `json:"hash"`
	BuildVersion string `json:"version"`
	GitTreeState string `json:"-"`
	BuildDate    string `json:"-"`
	GoVersion    string `json:"-"`
	Compiler     string `json:"-"`
	Platform     string `json:"-"`
}

func VersionInfo() Info {
	// These variables typically come from -ldflags settings and in
	// their absence fallback to the global defaults set above.

	// Create the semver version based on the state of the current commit or its branch.
	// Use the first version we find.
	var version string
	tags := strings.Fields(buildTagCurrent)
	for _, t := range tags {
		if strings.HasPrefix(t, "v") {
			version = t
			break
		}
	}
	if version == "" {
		version = buildTagLatest + "+" + buildHash
	}
	version = strings.TrimPrefix(version, "v")

	return Info{
		GitVersion:   gitVersion,
		BuildHash:    buildHash,
		BuildVersion: version,
		GitTreeState: gitTreeState,
		BuildDate:    buildDate,
		GoVersion:    runtime.Version(),
		Compiler:     runtime.Compiler,
		Platform:     fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH),
	}
}

// String returns the string representation of the version info
func (i *Info) String() string {
	b := strings.Builder{}
	w := tabwriter.NewWriter(&b, 0, 0, 2, ' ', 0)

	fmt.Fprintf(w, "GitVersion:\t%s\n", i.GitVersion)
	fmt.Fprintf(w, "BuildHash:\t%s\n", i.BuildHash)
	fmt.Fprintf(w, "BuildVersion:\t%s\n", i.BuildVersion)
	fmt.Fprintf(w, "GitTreeState:\t%s\n", i.GitTreeState)
	fmt.Fprintf(w, "BuildDate:\t%s\n", i.BuildDate)
	fmt.Fprintf(w, "GoVersion:\t%s\n", i.GoVersion)
	fmt.Fprintf(w, "Compiler:\t%s\n", i.Compiler)
	fmt.Fprintf(w, "Platform:\t%s\n", i.Platform)

	w.Flush() // #nosec
	return b.String()
}


================================================
FILE: main.go
================================================
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.

package main

import (
	"flag"
	"log"
	"os"
	"os/signal"
	"syscall"

	"github.com/mattermost/mattermost-push-proxy/internal/version"
	"github.com/mattermost/mattermost-push-proxy/server"
)

var (
	flagConfigFile string
	falgVersion    bool
)

func main() {
	flag.StringVar(&flagConfigFile, "config", "mattermost-push-proxy.json", "")
	flag.BoolVar(&falgVersion, "version", false, "")
	flag.Parse()

	if falgVersion {
		if err := version.GetVersion(); err != nil {
			log.Fatal(err)
		}

		os.Exit(0)
	}

	fileName := server.FindConfigFile(flagConfigFile)
	cfg, err := server.LoadConfig(fileName)
	if err != nil {
		// We just do a hard exit, because the app won't be able to start without a config.
		log.Fatal(err)
	}

	logger, err := server.NewLogger(cfg)
	defer func() {
		if logger != nil {
			_ = logger.Shutdown()
		}
	}()
	if err != nil {
		log.Fatal(err)
	}

	logger.Info("Loading " + fileName)

	srv := server.New(cfg, logger)
	srv.Start()

	// wait for kill signal before attempting to gracefully shutdown
	// the running service
	stopChan := make(chan os.Signal, 1)
	signal.Notify(stopChan, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
	<-stopChan

	srv.Stop()
}


================================================
FILE: package.json
================================================
{
  "name": "mattermost-push-proxy",
  "version": "1.0.0",
  "description": "Repository for mattermost push proxy",
  "main": "",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "validate": "swagger-cli validate ./swagger/spec.yaml",
    "build": "redoc-cli bundle -t ./swagger/template.hbs ./swagger/spec.yaml -o index.html --options.suppressWarnings",
    "serve": "redoc-cli serve -t ./swagger/template.hbs ./swagger/spec.yaml -o index.html --options.suppressWarnings --ssr --watch"
  },
  "repository": {
    "type": "git",
    "url": "git+https://github.com/mattermost/mattermost-push-proxy.git"
  },
  "author": "",
  "license": "ISC",
  "bugs": {
    "url": "https://github.com/mattermost/mattermost-push-proxy/issues"
  },
  "homepage": "https://github.com/mattermost/mattermost-push-proxy#readme",
  "dependencies": {
    "redoc-cli": "^0.9.9",
    "swagger-cli": "^4.0.4"
  }
}


================================================
FILE: scripts/go_install.sh
================================================
#!/usr/bin/env bash

# Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
# See LICENSE.txt for license information.

set -o errexit
set -o nounset
set -o pipefail

if [ -z "${1}" ]; then
  echo "must provide module as first parameter"
  exit 1
fi

if [ -z "${2}" ]; then
  echo "must provide binary name as second parameter"
  exit 1
fi

if [ -z "${3}" ]; then
  echo "must provide version as third parameter"
  exit 1
fi

if [ -z "${GOBIN}" ]; then
  echo "GOBIN is not set. Must set GOBIN to install the bin in a specified directory."
  exit 1
fi

tmp_dir=$(mktemp -d -t goinstall_XXXXXXXXXX)
function clean {
  rm -rf "${tmp_dir}"
}
trap clean EXIT

rm "${GOBIN}/${2}"* || true

cd "${tmp_dir}"

# create a new module in the tmp directory
go mod init fake/mod

# install the golang module specified as the first argument
go install -tags tools "${1}@${3}"
mv "${GOBIN}/${2}" "${GOBIN}/${2}-${3}"
ln -sf "${GOBIN}/${2}-${3}" "${GOBIN}/${2}"


================================================
FILE: server/android_notification_server.go
================================================
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.

package server

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"os"
	"reflect"
	"strconv"
	"time"

	firebase "firebase.google.com/go/v4"
	"firebase.google.com/go/v4/messaging"
	"github.com/kyokomi/emoji"
	"golang.org/x/oauth2/google"
	"google.golang.org/api/option"

	"github.com/mattermost/mattermost/server/public/shared/mlog"
)

const (
	apnsAuthError       = "APNS_AUTH_ERROR"
	internalError       = "INTERNAL"
	thirdPartyAuthError = "THIRD_PARTY_AUTH_ERROR"
	invalidArgument     = "INVALID_ARGUMENT"
	quotaExceeded       = "QUOTA_EXCEEDED"
	unregistered        = "UNREGISTERED"
	unavailable         = "UNAVAILABLE"
	tokenSourceError    = "TOKEN_SOURCE_ERROR"
)

const (
	scope = "https://www.googleapis.com/auth/firebase.messaging"
)

type AndroidNotificationServer struct {
	metrics             *metrics
	logger              *mlog.Logger
	AndroidPushSettings AndroidPushSettings
	client              *messaging.Client
	sendTimeout         time.Duration
	retryTimeout        time.Duration
}

// serviceAccount contains a subset of the fields in service-account.json.
// It is mainly used to extract the projectID and client email for authentication.
type serviceAccount struct {
	Type        string `json:"type"`
	ProjectID   string `json:"project_id"`
	ClientEmail string `json:"client_email"`
	ClientID    string `json:"client_id"`
	AuthURI     string `json:"auth_uri"`
	TokenURI    string `json:"token_uri"`
}

func NewAndroidNotificationServer(settings AndroidPushSettings, logger *mlog.Logger, metrics *metrics, sendTimeoutSecs int, retryTimeoutSecs int) *AndroidNotificationServer {
	return &AndroidNotificationServer{
		AndroidPushSettings: settings,
		metrics:             metrics,
		logger:              logger,
		sendTimeout:         time.Duration(sendTimeoutSecs) * time.Second,
		retryTimeout:        time.Duration(retryTimeoutSecs) * time.Second,
	}
}

func (me *AndroidNotificationServer) Initialize() error {
	me.logger.Info("Initializing Android notification server", mlog.String("type", me.AndroidPushSettings.Type))

	if me.AndroidPushSettings.AndroidAPIKey != "" {
		me.logger.Info("AndroidPushSettings.AndroidAPIKey is no longer used. Please remove this config value.")
	}

	if me.AndroidPushSettings.ServiceFileLocation == "" {
		return errors.New("Android push notifications not configured.  Missing ServiceFileLocation.")
	}

	jsonKey, err := os.ReadFile(me.AndroidPushSettings.ServiceFileLocation)
	if err != nil {
		return fmt.Errorf("error reading service file: %v", err)
	}

	cfg, err := google.JWTConfigFromJSON(jsonKey, scope)
	if err != nil {
		return fmt.Errorf("error getting JWT config: %v", err)
	}

	var serviceAcc serviceAccount
	err = json.Unmarshal(jsonKey, &serviceAcc)
	if err != nil {
		return fmt.Errorf("error parsing service account JSON: %v", err)
	}

	opt := option.WithTokenSource(cfg.TokenSource(context.Background()))
	conf := &firebase.Config{
		ProjectID:        serviceAcc.ProjectID,
		ServiceAccountID: serviceAcc.ClientEmail,
	}
	app, err := firebase.NewApp(context.Background(), conf, opt)
	if err != nil {
		return fmt.Errorf("error initializing app: %v", err)
	}

	client, err := app.Messaging(context.Background())
	if err != nil {
		return fmt.Errorf("error initializing client: %v", err)
	}
	me.client = client

	return nil
}

func (me *AndroidNotificationServer) SendNotification(msg *PushNotification) PushResponse {
	pushType := msg.Type
	data := map[string]string{
		"ack_id":         msg.AckID,
		"type":           pushType,
		"sub_type":       msg.SubType,
		"version":        msg.Version,
		"channel_id":     msg.ChannelID,
		"is_crt_enabled": strconv.FormatBool(msg.IsCRTEnabled),
		"server_id":      msg.ServerID,
		"category":       msg.Category,
	}

	if msg.Badge != -1 {
		data["badge"] = strconv.Itoa(msg.Badge)
	}

	if msg.RootID != "" {
		data["root_id"] = msg.RootID
	}

	if msg.Signature == "" {
		data["signature"] = "NO_SIGNATURE"
	} else {
		data["signature"] = msg.Signature
	}

	if msg.IsIDLoaded {
		data["post_id"] = msg.PostID
		data["message"] = msg.Message
		data["id_loaded"] = "true"
		data["sender_id"] = msg.SenderID
		data["sender_name"] = "Someone"
		data["team_id"] = msg.TeamID
	} else if pushType == PushTypeMessage || pushType == PushTypeSession {
		data["team_id"] = msg.TeamID
		data["sender_id"] = msg.SenderID
		data["sender_name"] = msg.SenderName
		data["message"] = emoji.Sprint(msg.Message)
		data["channel_name"] = msg.ChannelName
		data["post_id"] = msg.PostID
		data["override_username"] = msg.OverrideUsername
		data["override_icon_url"] = msg.OverrideIconURL
		data["from_webhook"] = msg.FromWebhook
	}

	if me.metrics != nil {
		me.metrics.incrementNotificationTotal(PushNotifyAndroid, pushType)
	}
	fcmMsg := &messaging.Message{
		Token: msg.DeviceID,
		Data:  data,
		Android: &messaging.AndroidConfig{
			Priority: "high",
		},
	}

	me.logger.Info(
		"Sending android push notification",
		mlog.String("device", me.AndroidPushSettings.Type),
		mlog.String("type", msg.Type),
		mlog.String("ack_id", msg.AckID),
	)
	err := me.SendNotificationWithRetry(fcmMsg)

	if err != nil {
		errorCode, hasStatusCode := getErrorCode(err)
		if !hasStatusCode {
			errorCode = "NONE"
		}

		me.logger.Error(
			"Failed to send FCM push",
			mlog.String("sid", msg.ServerID),
			mlog.String("did", msg.DeviceID),
			mlog.Err(err),
			mlog.String("type", me.AndroidPushSettings.Type),
			mlog.String("errorCode", errorCode),
		)

		if messaging.IsUnregistered(err) || messaging.IsSenderIDMismatch(err) {
			me.logger.Info("Android response failure sending remove code", mlog.String("type", me.AndroidPushSettings.Type))
			if me.metrics != nil {
				me.metrics.incrementRemoval(PushNotifyAndroid, pushType, unregistered)
			}
			return NewRemovePushResponse()
		}

		var reason string
		switch {
		case messaging.IsInternal(err):
			reason = internalError
		case messaging.IsInvalidArgument(err):
			reason = invalidArgument
		case messaging.IsQuotaExceeded(err):
			reason = quotaExceeded
		case messaging.IsThirdPartyAuthError(err):
			reason = thirdPartyAuthError
		case messaging.IsUnavailable(err):
			reason = unavailable
		default:
			reason = "unknown transport error"

		}
		if me.metrics != nil {
			me.metrics.incrementFailure(PushNotifyAndroid, pushType, reason)
		}

		return NewErrorPushResponse(err.Error())
	}

	if me.metrics != nil {
		if msg.AckID != "" {
			me.metrics.incrementSuccessWithAck(PushNotifyAndroid, pushType)
		} else {
			me.metrics.incrementSuccess(PushNotifyAndroid, pushType)
		}
	}
	return NewOkPushResponse()
}

func (me *AndroidNotificationServer) SendNotificationWithRetry(fcmMsg *messaging.Message) error {
	var err error
	waitTime := time.Second

	logger := me.logger.With(mlog.String("did", fcmMsg.Token))

	// Keep a general context to make sure the whole retry
	// doesn't take longer than the timeout.
	generalContext, cancelGeneralContext := context.WithTimeout(context.Background(), me.sendTimeout)
	defer cancelGeneralContext()

	for retries := 0; retries < MAX_RETRIES; retries++ {
		start := time.Now()

		retryContext, cancelRetryContext := context.WithTimeout(generalContext, me.retryTimeout)
		defer cancelRetryContext()
		_, err = me.client.Send(retryContext, fcmMsg)
		if me.metrics != nil {
			me.metrics.observerNotificationResponse(PushNotifyAndroid, time.Since(start).Seconds())
		}

		if err == nil || !isRetryable(err) {
			break
		}

		logger.Error(
			"Failed to send android push",
			mlog.Int("retry", retries),
			mlog.Err(err),
		)

		if retries == MAX_RETRIES-1 {
			logger.Error("Max retries reached")
			break
		}

		select {
		case <-generalContext.Done():
			if generalContext.Err() != nil {
				logger.Info(
					"Not retrying because context error",
					mlog.Int("retry", retries),
					mlog.Err(generalContext.Err()),
				)
			}
			return generalContext.Err()
		case <-time.After(waitTime):
		}

		waitTime *= 2
	}

	return err
}

func isRetryable(err error) bool {
	// We retry if the context deadline is exceeded.
	// This may cause double notifications, but we expect
	// this not to happen often.
	if errors.Is(err, context.DeadlineExceeded) {
		return true
	}

	// We retry the errors based on https://firebase.google.com/docs/cloud-messaging/http-server-ref
	return messaging.IsInternal(err) ||
		messaging.IsQuotaExceeded(err)

	// messaging.IsUnavailable is retried by the default retry config in
	// firebase.google.com/go/v4@v4.14.0/internal/http_client.go
	// messaging.IsUnavailable(err)
}

func getErrorCode(err error) (string, bool) {
	if err == nil {
		return "", false
	}

	errorPointer := reflect.ValueOf(err)
	if errorPointer.Kind() != reflect.Ptr {
		return "", false
	}

	errorValue := errorPointer.Elem()
	if errorValue.Kind() != reflect.Struct {
		return "", false
	}

	codeValue := errorValue.FieldByName("ErrorCode")
	if !codeValue.IsValid() {
		return "", false
	}

	return codeValue.String(), true
}


================================================
FILE: server/android_notification_test.go
================================================
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.

package server

import (
	"encoding/json"
	"errors"
	"net/http"
	"os"
	"testing"

	"github.com/stretchr/testify/require"

	"github.com/mattermost/mattermost/server/public/shared/mlog"
)

func TestAndroidInitialize(t *testing.T) {
	fileName := FindConfigFile("mattermost-push-proxy.sample.json")
	cfg, err := LoadConfig(fileName)
	require.NoError(t, err)

	logger, err := mlog.NewLogger()
	require.NoError(t, err)

	// Verify error for no service file
	pushSettings := AndroidPushSettings{}
	cfg.AndroidPushSettings[0] = pushSettings
	require.Error(t, NewAndroidNotificationServer(cfg.AndroidPushSettings[0], logger, nil, cfg.SendTimeoutSec, cfg.RetryTimeoutSec).Initialize())

	f, err := os.CreateTemp("", "example")
	require.NoError(t, err)
	defer os.Remove(f.Name()) // clean up

	cfg.AndroidPushSettings[0].ServiceFileLocation = f.Name()

	// Verify error for bad JSON
	_, err = f.Write([]byte("badJSON"))
	require.NoError(t, err)
	require.Error(t, NewAndroidNotificationServer(cfg.AndroidPushSettings[0], logger, nil, cfg.SendTimeoutSec, cfg.RetryTimeoutSec).Initialize())

	require.NoError(t, f.Truncate(0))
	_, err = f.Seek(0, 0)
	require.NoError(t, err)

	// Verify no error for dummy JSON
	require.NoError(t, json.NewEncoder(f).Encode(serviceAccount{
		Type:      "service_account",
		ProjectID: "sample",
	}))
	require.NoError(t, f.Sync())
	require.NoError(t, NewAndroidNotificationServer(cfg.AndroidPushSettings[0], logger, nil, cfg.SendTimeoutSec, cfg.RetryTimeoutSec).Initialize())

	require.NoError(t, f.Close())
}

// Copied from firebase.google.com/go/v4@v4.14.0/internal/errors.go
type ErrorCode string
type FirebaseError struct {
	ErrorCode ErrorCode
	String    string
	Response  *http.Response
	Ext       map[string]interface{}
}

func (fe *FirebaseError) Error() string {
	return fe.String
}

func TestGetErrorCode(t *testing.T) {
	var errorCode ErrorCode = "some error code"
	err := &FirebaseError{
		ErrorCode: errorCode,
	}

	extractedCode, found := getErrorCode(err)
	require.True(t, found)
	require.Equal(t, string(errorCode), extractedCode)

	extractedCode, found = getErrorCode(errors.New("non firebase error"))
	require.Equal(t, "", extractedCode)
	require.False(t, found)
}


================================================
FILE: server/apple_notification_server.go
================================================
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.

package server

import (
	"context"
	"crypto/tls"
	"fmt"
	"net/http"
	"net/url"
	"time"

	"github.com/kyokomi/emoji"
	apns "github.com/sideshow/apns2"
	"github.com/sideshow/apns2/certificate"
	"github.com/sideshow/apns2/payload"
	"github.com/sideshow/apns2/token"
	"golang.org/x/net/http2"

	"github.com/mattermost/mattermost/server/public/shared/mlog"
)

type AppleNotificationServer struct {
	AppleClient       *apns.Client
	metrics           *metrics
	logger            *mlog.Logger
	ApplePushSettings ApplePushSettings
	sendTimeout       time.Duration
	retryTimeout      time.Duration
}

func NewAppleNotificationServer(settings ApplePushSettings, logger *mlog.Logger, metrics *metrics, sendTimeoutSecs int, retryTimeoutSecs int) *AppleNotificationServer {
	return &AppleNotificationServer{
		ApplePushSettings: settings,
		metrics:           metrics,
		logger:            logger,
		sendTimeout:       time.Duration(sendTimeoutSecs) * time.Second,
		retryTimeout:      time.Duration(retryTimeoutSecs) * time.Second,
	}
}

func (me *AppleNotificationServer) setupProxySettings(appleCert *tls.Certificate) error {
	// Override the native transport.
	proxyServer := getProxyServer()
	if proxyServer != "" {
		transport := &http.Transport{
			Proxy: func(request *http.Request) (*url.URL, error) {
				return url.Parse(proxyServer)
			},
			IdleConnTimeout: apns.HTTPClientTimeout,
		}

		if appleCert != nil {
			transport.TLSClientConfig = &tls.Config{
				Certificates: []tls.Certificate{*appleCert},
			}
		}

		err := http2.ConfigureTransport(transport)
		if err != nil {
			return fmt.Errorf("Transport Error: %v", err)
		}

		me.AppleClient.HTTPClient.Transport = transport
	}

	if appleCert != nil {
		me.logger.Info("Initializing apple notification server with PEM certificate", mlog.String("type", me.ApplePushSettings.Type))
	} else {
		me.logger.Info("Initializing apple notification server with AuthKey", mlog.String("type", me.ApplePushSettings.Type))
	}

	return nil
}

func (me *AppleNotificationServer) Initialize() error {
	if me.ApplePushSettings.AppleAuthKeyFile != "" && me.ApplePushSettings.AppleAuthKeyID != "" && me.ApplePushSettings.AppleTeamID != "" {
		authKey, err := token.AuthKeyFromFile(me.ApplePushSettings.AppleAuthKeyFile)
		if err != nil {
			return fmt.Errorf("Failed to initialize apple notification service with AuthKey file err=%v ", err)
		}

		appleToken := &token.Token{
			AuthKey: authKey,
			KeyID:   me.ApplePushSettings.AppleAuthKeyID,
			TeamID:  me.ApplePushSettings.AppleTeamID,
		}

		if me.ApplePushSettings.ApplePushUseDevelopment {
			me.AppleClient = apns.NewTokenClient(appleToken).Development()
		} else {
			me.AppleClient = apns.NewTokenClient(appleToken).Production()
		}

		// Override the native transport.
		return me.setupProxySettings(nil)
	}

	if me.ApplePushSettings.ApplePushCertPrivate != "" {
		appleCert, appleCertErr := certificate.FromPemFile(me.ApplePushSettings.ApplePushCertPrivate, me.ApplePushSettings.ApplePushCertPassword)
		if appleCertErr != nil {
			return fmt.Errorf("Failed to initialize apple notification service with pem cert err=%v for type=%v", appleCertErr, me.ApplePushSettings.Type)
		}

		if me.ApplePushSettings.ApplePushUseDevelopment {
			me.AppleClient = apns.NewClient(appleCert).Development()
		} else {
			me.AppleClient = apns.NewClient(appleCert).Production()
		}

		// Override the native transport.
		return me.setupProxySettings(&appleCert)
	}

	return fmt.Errorf("Apple push notifications not configured.  Missing ApplePushCertPrivate. for type=%v", me.ApplePushSettings.Type)
}

func (me *AppleNotificationServer) SendNotification(msg *PushNotification) PushResponse {

	data := payload.NewPayload()
	if msg.Badge == 0 && msg.Type == PushTypeClear && msg.AppVersion > 1 {
		data.Badge(1)
	} else if msg.Badge != -1 {
		data.Badge(msg.Badge)
	}

	notification := &apns.Notification{}
	notification.DeviceToken = msg.DeviceID
	notification.Payload = data
	notification.Topic = me.ApplePushSettings.ApplePushTopic
	notification.Priority = apns.PriorityHigh

	var pushType = msg.Type
	if msg.IsIDLoaded {
		data.Category(msg.Category)
		data.Sound("default")
		data.Custom("version", msg.Version)
		data.Custom("id_loaded", true)
		data.MutableContent()
		data.AlertBody(msg.Message)
		data.ContentAvailable()
	} else {
		switch msg.Type {
		case PushTypeMessage, PushTypeSession:
			data.Category(msg.Category)
			data.Sound("default")
			data.Custom("version", msg.Version)
			data.MutableContent()
			if msg.Type == PushTypeMessage {
				data.ContentAvailable()
			}

			if msg.ChannelName != "" && msg.Version == "v2" {
				data.AlertTitle(msg.ChannelName)
				data.AlertBody(emoji.Sprint(msg.Message))
				data.Custom("channel_name", msg.ChannelName)
			} else {
				data.Alert(emoji.Sprint(msg.Message))

				if msg.ChannelName != "" {
					data.Custom("channel_name", msg.ChannelName)
				}
			}
		case PushTypeClear, PushTypeTest:
			data.ContentAvailable()
		case PushTypeUpdateBadge:
			// Handled by the apps, nothing else to do here
		}
	}
	if me.metrics != nil {
		me.metrics.incrementNotificationTotal(PushNotifyApple, pushType)
	}
	data.Custom("type", pushType)
	data.Custom("sub_type", msg.SubType)
	data.Custom("server_id", msg.ServerID)

	if msg.AckID != "" {
		data.Custom("ack_id", msg.AckID)
	}

	data.Custom("is_crt_enabled", msg.IsCRTEnabled)

	if msg.ChannelID != "" {
		data.Custom("channel_id", msg.ChannelID)

		if msg.IsCRTEnabled && msg.RootID != "" {
			data.ThreadID(msg.RootID)
		} else {
			data.ThreadID(msg.ChannelID)
		}
	}

	if msg.Signature == "" {
		data.Custom("signature", "NO_SIGNATURE")
	} else {
		data.Custom("signature", msg.Signature)
	}

	if msg.TeamID != "" {
		data.Custom("team_id", msg.TeamID)
	}

	if msg.SenderID != "" {
		data.Custom("sender_id", msg.SenderID)
	}

	if msg.SenderName != "" {
		data.Custom("sender_name", msg.SenderName)
	}

	if msg.PostID != "" {
		data.Custom("post_id", msg.PostID)
	}

	if msg.RootID != "" {
		data.Custom("root_id", msg.RootID)
	}

	if msg.OverrideUsername != "" {
		data.Custom("override_username", msg.OverrideUsername)
	}

	if msg.OverrideIconURL != "" {
		data.Custom("override_icon_url", msg.OverrideIconURL)
	}

	if msg.FromWebhook != "" {
		data.Custom("from_webhook", msg.FromWebhook)
	}

	if me.AppleClient != nil {
		me.logger.Info(
			"Sending apple push notification",
			mlog.String("device", me.ApplePushSettings.Type),
			mlog.String("type", msg.Type),
			mlog.String("ack_id", msg.AckID),
		)

		res, err := me.SendNotificationWithRetry(notification)
		if err != nil {
			me.logger.Error(
				"Failed to send apple push",
				mlog.String("sid", msg.ServerID),
				mlog.String("did", msg.DeviceID),
				mlog.Err(err),
				mlog.String("type", me.ApplePushSettings.Type),
			)
			if me.metrics != nil {
				me.metrics.incrementFailure(PushNotifyApple, pushType, "RequestError")
			}
			return NewErrorPushResponse("unknown transport error")
		}

		if !res.Sent() {
			if res.Reason == apns.ReasonBadDeviceToken || res.Reason == apns.ReasonUnregistered || res.Reason == apns.ReasonMissingDeviceToken || res.Reason == apns.ReasonDeviceTokenNotForTopic {
				me.logger.Info(
					"Failed to send apple push sending remove code res",
					mlog.String("ApnsID", res.ApnsID),
					mlog.String("reason", res.Reason),
					mlog.Int("code", res.StatusCode),
					mlog.String("type", me.ApplePushSettings.Type),
				)
				if me.metrics != nil {
					me.metrics.incrementRemoval(PushNotifyApple, pushType, res.Reason)
				}
				return NewRemovePushResponse()
			}

			me.logger.Error(
				"Failed to send apple push with res",
				mlog.String("ApnsID", res.ApnsID),
				mlog.String("reason", res.Reason),
				mlog.Int("code", res.StatusCode),
				mlog.String("type", me.ApplePushSettings.Type),
			)
			if me.metrics != nil {
				me.metrics.incrementFailure(PushNotifyApple, pushType, res.Reason)
			}
			return NewErrorPushResponse("unknown send response error")
		}
	}
	if me.metrics != nil {
		if msg.AckID != "" {
			me.metrics.incrementSuccessWithAck(PushNotifyApple, pushType)
		} else {
			me.metrics.incrementSuccess(PushNotifyApple, pushType)
		}
	}
	return NewOkPushResponse()
}

func (me *AppleNotificationServer) SendNotificationWithRetry(notification *apns.Notification) (*apns.Response, error) {
	var res *apns.Response
	var err error
	waitTime := time.Second

	// Keep a general context to make sure the whole retry
	// doesn't take longer than the timeout.
	generalContext, cancelGeneralContext := context.WithTimeout(context.Background(), me.sendTimeout)
	defer cancelGeneralContext()

	for retries := 0; retries < MAX_RETRIES; retries++ {
		start := time.Now()

		retryContext, cancelRetryContext := context.WithTimeout(generalContext, me.retryTimeout)
		defer cancelRetryContext()
		res, err = me.AppleClient.PushWithContext(retryContext, notification)
		if me.metrics != nil {
			me.metrics.observerNotificationResponse(PushNotifyApple, time.Since(start).Seconds())
		}

		if err == nil {
			break
		}

		me.logger.Error(
			"Failed to send apple push",
			mlog.String("did", notification.DeviceToken),
			mlog.Int("retry", retries),
			mlog.Err(err),
		)

		if retries == MAX_RETRIES-1 {
			me.logger.Error("Max retries reached", mlog.String("did", notification.DeviceToken))
			break
		}

		select {
		case <-generalContext.Done():
		case <-time.After(waitTime):
		}

		if generalContext.Err() != nil {
			me.logger.Info(
				"Not retrying because context error",
				mlog.String("did", notification.DeviceToken),
				mlog.Int("retry", retries),
				mlog.Err(generalContext.Err()),
			)
			err = generalContext.Err()
			break
		}

		waitTime *= 2
	}

	return res, err
}


================================================
FILE: server/config_push_proxy.go
================================================
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.

package server

import (
	"encoding/json"
	"fmt"
	"io"
	"os"
	"path/filepath"
)

type ConfigPushProxy struct {
	AndroidPushSettings     []AndroidPushSettings
	ListenAddress           string
	ThrottleVaryByHeader    string
	LogFileLocation         string
	SendTimeoutSec          int
	RetryTimeoutSec         int
	ApplePushSettings       []ApplePushSettings
	EnableMetrics           bool
	EnableConsoleLog        bool
	EnableFileLog           bool
	LogFormat               string // json or plain
	ThrottlePerSec          int
	ThrottleMemoryStoreSize int
}

type ApplePushSettings struct {
	Type                    string
	ApplePushCertPrivate    string
	ApplePushCertPassword   string
	ApplePushTopic          string
	AppleAuthKeyFile        string
	AppleAuthKeyID          string
	AppleTeamID             string
	ApplePushUseDevelopment bool
}

type AndroidPushSettings struct {
	Type                string
	AndroidAPIKey       string `json:"AndroidApiKey"`
	ServiceFileLocation string `json:"ServiceFileLocation"`
}

// FindConfigFile searches for the filepath in a list of directories
// and then returns the absolute path to that file.
func FindConfigFile(fileName string) string {
	if _, err := os.Stat("/tmp/" + fileName); err == nil {
		fileName, _ = filepath.Abs("/tmp/" + fileName)
	} else if _, err := os.Stat("./config/" + fileName); err == nil {
		fileName, _ = filepath.Abs("./config/" + fileName)
	} else if _, err := os.Stat("../config/" + fileName); err == nil {
		fileName, _ = filepath.Abs("../config/" + fileName)
	} else if _, err := os.Stat(fileName); err == nil {
		fileName, _ = filepath.Abs(fileName)
	}

	return fileName
}

// LoadConfig loads the config from the given file path.
func LoadConfig(fileName string) (*ConfigPushProxy, error) {
	file, err := os.Open(fileName)
	if err != nil {
		return nil, err
	}
	defer file.Close()

	buf, err := io.ReadAll(file)
	if err != nil {
		return nil, err
	}

	var cfg *ConfigPushProxy
	err = json.Unmarshal(buf, &cfg)
	if err != nil {
		fmt.Println(buf, err)
		return nil, err
	}

	if !cfg.EnableConsoleLog && !cfg.EnableFileLog {
		cfg.EnableConsoleLog = true
	}

	// Set timeout defaults
	if cfg.SendTimeoutSec == 0 {
		cfg.SendTimeoutSec = 30
	}

	if cfg.RetryTimeoutSec == 0 {
		cfg.RetryTimeoutSec = 8
	}

	if cfg.RetryTimeoutSec > cfg.SendTimeoutSec {
		cfg.RetryTimeoutSec = cfg.SendTimeoutSec
	}

	if cfg.EnableFileLog {
		if cfg.LogFileLocation == "" {
			// We just do an mkdir -p equivalent.
			// Otherwise, it would need 2 steps of statting and creating.
			err := os.MkdirAll("./logs", 0755)
			if err != nil {
				// If it fails, we log in the current directory itself
				cfg.LogFileLocation = "./push_proxy.log"
			} else {
				cfg.LogFileLocation = "./logs/push_proxy.log"
			}
		}
		// if file does not exist, create it.
		if _, err := os.Stat(cfg.LogFileLocation); os.IsNotExist(err) {
			f, err := os.Create(cfg.LogFileLocation)
			if err != nil {
				return nil, err
			}
			if err := f.Close(); err != nil {
				return nil, err
			}
		}
	}

	return cfg, nil
}


================================================
FILE: server/logger.go
================================================
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.

package server

import (
	"encoding/json"
	"github.com/mattermost/mattermost/server/public/shared/mlog"
)

func NewLogger(cfg *ConfigPushProxy) (*mlog.Logger, error) {
	// Initialize the logger - begin
	logger, err := mlog.NewLogger()
	if err != nil {
		return nil, err
	}
	if cfg.LogFormat != "plain" && cfg.LogFormat != "json" {
		cfg.LogFormat = "plain"
	}
	err = logger.ConfigureTargets(buildLogConfig(cfg), nil)
	if err != nil {
		return logger, err
	}

	return logger, nil
}

func buildLogConfig(cfg *ConfigPushProxy) mlog.LoggerConfiguration {
	logConf := make(mlog.LoggerConfiguration)

	if cfg.EnableFileLog && cfg.LogFileLocation != "" {
		logConf["file"] = buildLogFileConfig(cfg.LogFileLocation, cfg.LogFormat)
	}

	if cfg.EnableConsoleLog || cfg.LogFileLocation == "" || !cfg.EnableFileLog {
		logConf["console"] = buildConsoleLogConfig(cfg.LogFormat)
	}

	return logConf
}

func buildConsoleLogConfig(format string) mlog.TargetCfg {
	return mlog.TargetCfg{
		Type:          "console",
		Levels:        mlog.StdAll,
		Format:        format,
		Options:       json.RawMessage(`{"out": "stdout"}`),
		FormatOptions: json.RawMessage(`{"enable_color": true, "enable_caller": true}`),
		MaxQueueSize:  1000,
	}
}

func buildLogFileConfig(filename string, format string) mlog.TargetCfg {
	opts := struct {
		Filename    string `json:"filename"`
		Max_size    int    `json:"max_size"`
		Max_age     int    `json:"max_age"`
		Max_backups int    `json:"max_backups"`
		Compress    bool   `json:"compress"`
	}{
		Filename:    filename,
		Max_size:    100,
		Max_age:     0,
		Max_backups: 0,
		Compress:    true,
	}
	var optsJsonString, _ = json.Marshal(opts)

	return mlog.TargetCfg{
		Type:          "file",
		Levels:        mlog.StdAll,
		Format:        format,
		Options:       optsJsonString,
		FormatOptions: json.RawMessage(`{"enable_color": true, "enable_caller": true}`),
		MaxQueueSize:  1000,
	}
}


================================================
FILE: server/logger_test.go
================================================
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.

package server

import (
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"os"
	"testing"
)

func TestNewMlogLogger(t *testing.T) {
	t.Run("Instancing logger with implicit plain console", func(t *testing.T) {
		cfg := &ConfigPushProxy{
			EnableFileLog: false,
		}
		logger, err := NewLogger(cfg)
		assert.NoError(t, err)
		assert.NotNil(t, logger)
	})

	t.Run("Instancing logger with json file", func(t *testing.T) {
		log, err := os.CreateTemp("", "log")
		require.NoError(t, err)

		err = log.Close()
		require.NoError(t, err)
		defer os.Remove(log.Name())

		cfg := &ConfigPushProxy{
			EnableFileLog:   true,
			LogFileLocation: log.Name(),
			LogFormat:       "json",
		}

		logger, err := NewLogger(cfg)
		assert.NoError(t, err)
		assert.NotNil(t, logger)
	})

	t.Run("Instancing logger with both file and console", func(t *testing.T) {
		log, err := os.CreateTemp("", "log")
		require.NoError(t, err)

		err = log.Close()
		require.NoError(t, err)
		defer os.Remove(log.Name())

		cfg := &ConfigPushProxy{
			EnableConsoleLog: true,
			EnableFileLog:    true,
			LogFileLocation:  log.Name(),
			LogFormat:        "json",
		}

		logger, err := NewLogger(cfg)
		assert.NoError(t, err)
		assert.NotNil(t, logger)
	})
}


================================================
FILE: server/metrics.go
================================================
package server

import (
	"net/http"

	"github.com/prometheus/client_golang/prometheus"
	"github.com/prometheus/client_golang/prometheus/promhttp"
)

const (
	metricNotificationsTotalName   = "service_notifications_total"
	metricSuccessName              = "service_success_total"
	metricSuccessWithAckName       = "service_success_with_ack_total"
	metricDeliveredName            = "service_delivered_total"
	metricFailureName              = "service_failure_total"
	metricFailureWithReasonName    = "service_failure_with_reason_total"
	metricRemovalName              = "service_removal_total"
	metricBadRequestName           = "service_bad_request_total"
	metricFCMResponseName          = "service_fcm_request_duration_seconds"
	metricAPNSResponseName         = "service_apns_request_duration_seconds"
	metricServiceResponseName      = "service_request_duration_seconds"
	metricNotificationResponseName = "service_notification_duration_seconds"
)

// NewPrometheusHandler returns the http.Handler to expose Prometheus metrics
func NewPrometheusHandler() http.Handler {
	return promhttp.Handler()
}

type metrics struct {
	metricNotificationsTotal   *prometheus.CounterVec
	metricSuccess              *prometheus.CounterVec
	metricSuccessWithAck       *prometheus.CounterVec
	metricDelivered            *prometheus.CounterVec
	metricFailure              *prometheus.CounterVec
	metricFailureWithReason    *prometheus.CounterVec
	metricRemoval              *prometheus.CounterVec
	metricBadRequest           prometheus.Counter
	metricAPNSResponse         prometheus.Histogram
	metricFCMResponse          prometheus.Histogram
	metricNotificationResponse *prometheus.HistogramVec
	metricServiceResponse      prometheus.Histogram
}

// newMetrics initializes the metrics and registers them
func newMetrics() *metrics {
	m := &metrics{
		metricNotificationsTotal: prometheus.NewCounterVec(prometheus.CounterOpts{
			Name: metricNotificationsTotalName,
			Help: "Number of notifications sent"},
			[]string{"platform", "type"}),
		metricSuccess: prometheus.NewCounterVec(prometheus.CounterOpts{
			Name: metricSuccessName,
			Help: "Number of push success."},
			[]string{"platform", "type"}),
		metricSuccessWithAck: prometheus.NewCounterVec(prometheus.CounterOpts{
			Name: metricSuccessWithAckName,
			Help: "Number of push success that contains ackId."},
			[]string{"platform", "type"},
		),
		metricDelivered: prometheus.NewCounterVec(prometheus.CounterOpts{
			Name: metricDeliveredName,
			Help: "Number of push delivered."},
			[]string{"platform", "type"},
		),
		metricFailure: prometheus.NewCounterVec(prometheus.CounterOpts{
			Name: metricFailureName,
			Help: "Number of push errors."},
			[]string{"platform", "type"}),
		metricFailureWithReason: prometheus.NewCounterVec(prometheus.CounterOpts{
			Name: metricFailureWithReasonName,
			Help: "Number of push errors with reasons."},
			[]string{"platform", "type", "reason"}),
		metricRemoval: prometheus.NewCounterVec(prometheus.CounterOpts{
			Name: metricRemovalName,
			Help: "Number of device token errors."},
			[]string{"platform", "reason"}),
		metricBadRequest: prometheus.NewCounter(prometheus.CounterOpts{
			Name: metricBadRequestName,
			Help: "Request to push proxy was a bad request",
		}),
		metricAPNSResponse: prometheus.NewHistogram(prometheus.HistogramOpts{
			Name: metricAPNSResponseName,
			Help: "Request latency distribution",
		}),
		metricFCMResponse: prometheus.NewHistogram(prometheus.HistogramOpts{
			Name: metricFCMResponseName,
			Help: "Request latency distribution",
		}),
		metricNotificationResponse: prometheus.NewHistogramVec(prometheus.HistogramOpts{
			Name: metricNotificationResponseName,
			Help: "Notifiction request latency distribution"},
			[]string{"platform"}),
		metricServiceResponse: prometheus.NewHistogram(prometheus.HistogramOpts{
			Name: metricServiceResponseName,
			Help: "Request latency distribution",
		}),
	}

	prometheus.MustRegister(
		m.metricNotificationsTotal,
		m.metricSuccess,
		m.metricSuccessWithAck,
		m.metricFailure,
		m.metricFailureWithReason,
		m.metricRemoval,
		m.metricBadRequest,
		m.metricAPNSResponse,
		m.metricFCMResponse,
		m.metricServiceResponse,
		m.metricNotificationResponse,
	)

	return m
}

func (m *metrics) shutdown() {
	func(cs ...prometheus.Collector) {
		for _, c := range cs {
			prometheus.Unregister(c)
		}
	}(
		m.metricNotificationsTotal,
		m.metricSuccess,
		m.metricSuccessWithAck,
		m.metricFailure,
		m.metricFailureWithReason,
		m.metricRemoval,
		m.metricBadRequest,
		m.metricAPNSResponse,
		m.metricFCMResponse,
		m.metricServiceResponse,
		m.metricNotificationResponse,
	)
}

func (m *metrics) incrementNotificationTotal(platform, pushType string) {
	m.metricNotificationsTotal.WithLabelValues(platform, pushType).Inc()
}

func (m *metrics) incrementSuccess(platform, pushType string) {
	m.metricSuccess.WithLabelValues(platform, pushType).Inc()
}

func (m *metrics) incrementSuccessWithAck(platform, pushType string) {
	m.incrementSuccess(platform, pushType)
	m.metricSuccessWithAck.WithLabelValues(platform, pushType).Inc()
}

func (m *metrics) incrementDelivered(platform, pushType string) {
	m.metricDelivered.WithLabelValues(platform, pushType).Inc()
}

func (m *metrics) incrementFailure(platform, pushType, reason string) {
	m.metricFailure.WithLabelValues(platform, pushType).Inc()
	if reason != "" {
		m.metricFailureWithReason.WithLabelValues(platform, pushType, reason).Inc()
	}
}

func (m *metrics) incrementRemoval(platform, pushType, reason string) {
	m.metricRemoval.WithLabelValues(platform, reason).Inc()
	m.incrementFailure(platform, pushType, reason)
}

func (m *metrics) incrementBadRequest() {
	m.metricBadRequest.Inc()
}

func (m *metrics) observeAPNSResponse(dur float64) {
	m.metricAPNSResponse.Observe(dur)
}

func (m *metrics) observeFCMResponse(dur float64) {
	m.metricFCMResponse.Observe(dur)
}

func (m *metrics) observeServiceResponse(dur float64) {
	m.metricServiceResponse.Observe(dur)
}

func (m *metrics) observerNotificationResponse(platform string, dur float64) {
	m.metricNotificationResponse.WithLabelValues(platform).Observe(dur)
	switch platform {
	case PushNotifyApple:
		m.observeAPNSResponse(dur)
	case PushNotifyAndroid:
		m.observeFCMResponse(dur)
	}
}


================================================
FILE: server/metrics_test.go
================================================
package server

import (
	"io"
	"net/http"
	"strings"
	"testing"
	"time"

	"github.com/prometheus/common/expfmt"
	"github.com/stretchr/testify/require"

	"github.com/mattermost/mattermost/server/public/shared/mlog"
)

func TestMetricDisabled(t *testing.T) {
	t.Log("Testing Metrics Enabled")
	platform := "junk"
	pushType := PushTypeMessage

	fileName := FindConfigFile("mattermost-push-proxy.sample.json")
	cfg, err := LoadConfig(fileName)
	require.NoError(t, err)
	cfg.AndroidPushSettings[0].AndroidAPIKey = platform
	cfg.EnableMetrics = false

	logger, err := mlog.NewLogger()
	require.NoError(t, err)

	srv := New(cfg, logger)
	srv.Start()

	time.Sleep(time.Second * 2)
	defer func() {
		srv.Stop()
		time.Sleep(time.Second * 2)
	}()

	m := newMetrics()
	defer m.shutdown()

	m.incrementBadRequest()
	m.incrementNotificationTotal(platform, pushType)
	m.incrementSuccess(platform, pushType)
	m.incrementRemoval(platform, pushType, "not registered")
	m.incrementFailure(platform, pushType, "error")
	m.observerNotificationResponse(PushNotifyApple, 1)
	m.observerNotificationResponse(PushNotifyAndroid, 1)
	m.observeServiceResponse(1)

	resp, err := http.Get("http://localhost:8066/metrics")
	if err != nil {
		t.Fatalf("service should not return an http error")
	}
	defer resp.Body.Close()

	data, err := io.ReadAll(resp.Body)
	if err != nil {
		t.Fatalf("service should return a parsable response")
	}
	if !strings.Contains(string(data), "404 page not found") {
		t.Fatalf("service should return a 404")
	}
}

func TestMetricEnabled(t *testing.T) {
	t.Log("Testing Metrics Enabled")
	platform := "junk"
	pushType := PushTypeMessage

	fileName := FindConfigFile("mattermost-push-proxy.sample.json")
	cfg, err := LoadConfig(fileName)
	require.NoError(t, err)
	cfg.AndroidPushSettings[0].AndroidAPIKey = platform
	cfg.EnableMetrics = true

	logger, err := mlog.NewLogger()
	require.NoError(t, err)

	srv := New(cfg, logger)
	srv.Start()

	time.Sleep(time.Second * 2)
	defer func() {
		srv.Stop()
		time.Sleep(time.Second * 2)
	}()

	srv.metrics.incrementBadRequest()
	srv.metrics.incrementNotificationTotal(platform, pushType)
	srv.metrics.incrementSuccess(platform, pushType)
	srv.metrics.incrementRemoval(platform, pushType, "not registered")
	srv.metrics.incrementFailure(platform, pushType, "error")
	srv.metrics.observerNotificationResponse(PushNotifyApple, 1)
	srv.metrics.observerNotificationResponse(PushNotifyAndroid, 1)
	srv.metrics.observeServiceResponse(1)

	resp, err := http.Get("http://localhost:8066/metrics")
	if err != nil {
		t.Fatalf("failed to get metrics endpoint - %s", err.Error())
	}
	defer resp.Body.Close()

	parser := &expfmt.TextParser{}
	metrics, _ := parser.TextToMetricFamilies(resp.Body)

	counters := []string{metricSuccessName, metricFailureName, metricFailureWithReasonName, metricRemovalName, metricBadRequestName, metricNotificationsTotalName}
	for _, cn := range counters {
		if m, ok := metrics[cn]; !ok {
			t.Fatalf("metric not found. name: %s", cn)
		} else {
			val := m.Metric[0].Counter.Value
			result := float64(1)

			if cn == metricFailureName {
				result = float64(2)
			}

			if val == nil {
				t.Fatalf("no metric value. name: %s", cn)
			}
			if *val != result {
				t.Fatalf("metric value does not match. mame: %s, got: %v, expected: %v",
					cn, *val, result)
			}
		}
	}

	histograms := []string{metricAPNSResponseName, metricFCMResponseName, metricServiceResponseName, metricNotificationResponseName}
	for _, hn := range histograms {
		if m, ok := metrics[hn]; !ok {
			t.Fatalf("metric not found. name: %s", hn)
		} else {
			val := m.Metric[0].Histogram.SampleCount
			if val == nil {
				t.Fatalf("no metric value. name: %s", hn)
			}
			if *val != 1 {
				t.Fatalf("metric value does not match. mame: %s, got: %v, expected: %v",
					hn, *val, 1)
			}
		}
	}
}


================================================
FILE: server/push_notification.go
================================================
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.

package server

const (
	PushNotifyApple   = "apple"
	PushNotifyAndroid = "android"

	PushTypeMessage     = "message"
	PushTypeClear       = "clear"
	PushTypeUpdateBadge = "update_badge"
	PushTypeSession     = "session"
	PushTypeTest        = "test"

	PushMessageV2 = "v2"

	PushSoundNone = "none"
)

type PushNotificationAck struct {
	ID       string `json:"id"`
	Platform string `json:"platform"`
	Type     string `json:"type"`
}

type PushNotification struct {
	ID               string `json:"id"`
	AckID            string `json:"ack_id"`
	Platform         string `json:"platform"`
	ServerID         string `json:"server_id"`
	DeviceID         string `json:"device_id"`
	Category         string `json:"category"`
	Sound            string `json:"sound"`
	Message          string `json:"message"`
	TeamID           string `json:"team_id"`
	ChannelID        string `json:"channel_id"`
	PostID           string `json:"post_id"`
	RootID           string `json:"root_id"`
	ChannelName      string `json:"channel_name"`
	Type             string `json:"type"`
	SubType          string `json:"sub_type,omitempty"`
	SenderName       string `json:"sender_name"`
	SenderID         string `json:"sender_id"`
	OverrideUsername string `json:"override_username"`
	OverrideIconURL  string `json:"override_icon_url"`
	FromWebhook      string `json:"from_webhook"`
	Version          string `json:"version"`
	AppVersion       int    `json:"app_version,omitempty"`
	Badge            int    `json:"badge"`
	ContentAvailable int    `json:"cont_ava"`
	IsCRTEnabled     bool   `json:"is_crt_enabled"`
	IsIDLoaded       bool   `json:"is_id_loaded"`
	Signature        string `json:"signature"`
}


================================================
FILE: server/push_response.go
================================================
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.

package server

import (
	"encoding/json"
	"io"
)

const (
	PUSH_STATUS           = "status"
	PUSH_STATUS_OK        = "OK"
	PUSH_STATUS_FAIL      = "FAIL"
	PUSH_STATUS_REMOVE    = "REMOVE"
	PUSH_STATUS_ERROR_MSG = "error"
)

type PushResponse map[string]string

func NewOkPushResponse() PushResponse {
	m := make(map[string]string)
	m[PUSH_STATUS] = PUSH_STATUS_OK
	return m
}

func NewRemovePushResponse() PushResponse {
	m := make(map[string]string)
	m[PUSH_STATUS] = PUSH_STATUS_REMOVE
	return m
}

func NewErrorPushResponse(message string) PushResponse {
	m := make(map[string]string)
	m[PUSH_STATUS] = PUSH_STATUS_FAIL
	m[PUSH_STATUS_ERROR_MSG] = message
	return m
}

func PushResponseFromJson(data io.Reader) PushResponse {
	decoder := json.NewDecoder(data)

	var objmap PushResponse
	if err := decoder.Decode(&objmap); err != nil {
		return make(map[string]string)
	} else {
		return objmap
	}
}


================================================
FILE: server/server.go
================================================
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.

package server

import (
	"context"
	"encoding/json"
	"fmt"
	"net"
	"net/http"
	"os"
	"strconv"
	"strings"
	"time"

	"github.com/gorilla/handlers"
	"github.com/gorilla/mux"
	throttled "gopkg.in/throttled/throttled.v1"
	throttledStore "gopkg.in/throttled/throttled.v1/store"

	"github.com/mattermost/mattermost-push-proxy/internal/version"
	"github.com/mattermost/mattermost/server/public/shared/mlog"
)

const (
	HEADER_FORWARDED           = "X-Forwarded-For"
	HEADER_REAL_IP             = "X-Real-IP"
	WAIT_FOR_SERVER_SHUTDOWN   = time.Second * 5
	CONNECTION_TIMEOUT_SECONDS = 60
	MAX_RETRIES                = 3
)

type NotificationServer interface {
	SendNotification(msg *PushNotification) PushResponse
	Initialize() error
}

// Server is the main struct which performs all activities.
type Server struct {
	cfg         *ConfigPushProxy
	httpServer  *http.Server
	pushTargets map[string]NotificationServer
	metrics     *metrics
	logger      *mlog.Logger
}

// New returns a new Server instance.
func New(cfg *ConfigPushProxy, logger *mlog.Logger) *Server {
	return &Server{
		cfg:         cfg,
		pushTargets: make(map[string]NotificationServer),
		logger:      logger,
	}
}

// Start starts the server.
func (s *Server) Start() {
	v := version.VersionInfo()
	s.logger.Info("Push proxy server is initializing...", mlog.String("version", v.String()))

	proxyServer := getProxyServer()
	if proxyServer != "" {
		s.logger.Info("Proxy server detected.", mlog.String("proxyServer", proxyServer))
	}

	var m *metrics
	if s.cfg.EnableMetrics {
		m = newMetrics()
		s.metrics = m
	}

	for _, settings := range s.cfg.ApplePushSettings {
		server := NewAppleNotificationServer(settings, s.logger, m, s.cfg.SendTimeoutSec, s.cfg.RetryTimeoutSec)
		err := server.Initialize()
		if err != nil {
			s.logger.Error("Failed to initialize client", mlog.Err(err))
			continue
		}
		s.pushTargets[settings.Type] = server
	}

	for _, settings := range s.cfg.AndroidPushSettings {
		server := NewAndroidNotificationServer(settings, s.logger, m, s.cfg.SendTimeoutSec, s.cfg.RetryTimeoutSec)
		err := server.Initialize()
		if err != nil {
			s.logger.Error("Failed to initialize client", mlog.Err(err))
			continue
		}
		s.pushTargets[settings.Type] = server
	}

	router := mux.NewRouter()
	vary := throttled.VaryBy{}
	vary.RemoteAddr = false
	vary.Headers = strings.Fields(s.cfg.ThrottleVaryByHeader)
	th := throttled.RateLimit(throttled.PerSec(s.cfg.ThrottlePerSec), &vary, throttledStore.NewMemStore(s.cfg.ThrottleMemoryStoreSize))

	th.DeniedHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		s.logger.Error("Error: code=429", mlog.String("path", r.URL.Path), mlog.String("ip", s.getIpAddress(r)))
		throttled.DefaultDeniedHandler.ServeHTTP(w, r)
	})

	handler := th.Throttle(router)

	router.HandleFunc("/", root).Methods("GET")
	router.HandleFunc("/version", s.version).Methods("GET")

	metricCompatibleSendNotificationHandler := s.handleSendNotification
	metricCompatibleAckNotificationHandler := s.handleAckNotification
	if s.cfg.EnableMetrics {
		metrics := NewPrometheusHandler()
		router.Handle("/metrics", metrics).Methods("GET")
		metricCompatibleSendNotificationHandler = s.responseTimeMiddleware(s.handleSendNotification)
		metricCompatibleAckNotificationHandler = s.responseTimeMiddleware(s.handleAckNotification)
	}
	r := router.PathPrefix("/api/v1").Subrouter()
	r.HandleFunc("/send_push", metricCompatibleSendNotificationHandler).Methods("POST")
	r.HandleFunc("/ack", metricCompatibleAckNotificationHandler).Methods("POST")

	s.httpServer = &http.Server{
		Addr:         s.cfg.ListenAddress,
		Handler:      handlers.RecoveryHandler(handlers.PrintRecoveryStack(true))(handler),
		ReadTimeout:  time.Duration(CONNECTION_TIMEOUT_SECONDS) * time.Second,
		WriteTimeout: time.Duration(CONNECTION_TIMEOUT_SECONDS) * time.Second,
	}
	go func() {
		err := s.httpServer.ListenAndServe()
		if err != http.ErrServerClosed {
			s.logger.Fatal(err.Error())
		}
	}()

	s.logger.Info("Server is listening on " + s.cfg.ListenAddress)
}

// Stop stops the server.
func (s *Server) Stop() {
	s.logger.Info("Stopping Server...")
	ctx, cancel := context.WithTimeout(context.Background(), WAIT_FOR_SERVER_SHUTDOWN)
	defer cancel()
	if s.metrics != nil {
		s.metrics.shutdown()
	}
	// Close shop
	err := s.httpServer.Shutdown(ctx)
	if err != nil {
		s.logger.Error(err.Error())
	}
}

func root(w http.ResponseWriter, r *http.Request) {
	_, _ = w.Write([]byte("<html><body>Mattermost Push Proxy</body></html>"))
}

func (s *Server) version(w http.ResponseWriter, _ *http.Request) {
	info := version.VersionInfo()

	w.Header().Set("Content-Type", "application/json")
	if err := json.NewEncoder(w).Encode(info); err != nil {
		s.logger.Error("Failed to write response", mlog.Err(err))
		if s.metrics != nil {
			s.metrics.incrementBadRequest()
		}
	}
}

func (s *Server) responseTimeMiddleware(f func(w http.ResponseWriter, r *http.Request)) func(w http.ResponseWriter, r *http.Request) {
	return func(w http.ResponseWriter, r *http.Request) {
		start := time.Now()
		f(w, r)
		if s.metrics != nil {
			s.metrics.observeServiceResponse(time.Since(start).Seconds())
		}
	}
}

func (s *Server) handleSendNotification(w http.ResponseWriter, r *http.Request) {
	var msg PushNotification
	err := json.NewDecoder(r.Body).Decode(&msg)
	if err != nil {
		rMsg := fmt.Sprintf("Failed to read message body: %v", err)
		s.logger.Error(rMsg)
		resp := NewErrorPushResponse(rMsg)
		if err2 := json.NewEncoder(w).Encode(resp); err2 != nil {
			s.logger.Error("Failed to write response", mlog.Err(err2))
		}
		if s.metrics != nil {
			s.metrics.incrementBadRequest()
		}
		return
	}

	if msg.ServerID == "" {
		rMsg := "Failed because of missing server Id"
		s.logger.Error(rMsg)
		resp := NewErrorPushResponse(rMsg)
		if err2 := json.NewEncoder(w).Encode(resp); err2 != nil {
			s.logger.Error("Failed to write response", mlog.Err(err2))
		}
		if s.metrics != nil {
			s.metrics.incrementBadRequest()
		}
		return
	}

	if msg.DeviceID == "" {
		rMsg := fmt.Sprintf("Failed because of missing device Id serverId=%v", msg.ServerID)
		s.logger.Error(rMsg)
		resp := NewErrorPushResponse(rMsg)
		if err2 := json.NewEncoder(w).Encode(resp); err2 != nil {
			s.logger.Error("Failed to write response", mlog.Err(err2))
		}
		if s.metrics != nil {
			s.metrics.incrementBadRequest()
		}
		return
	}

	if len(msg.Message) > 2047 {
		msg.Message = msg.Message[0:2046]
	}

	if len(msg.ChannelName) > 64 {
		msg.ChannelName = msg.ChannelName[0:64]
	}

	// Parse the app version if available
	index := strings.Index(msg.Platform, "-v")
	platform := msg.Platform
	msg.AppVersion = 1
	if index > -1 {
		msg.Platform = platform[:index]
		appVersionString := platform[index+2:]
		version, e := strconv.Atoi(appVersionString)
		if e == nil {
			msg.AppVersion = version
		} else {
			rMsg := fmt.Sprintf("Could not determine the app version in %v appVersion=%v", msg.Platform, appVersionString)
			s.logger.Error(rMsg)
		}
	}

	if server, ok := s.pushTargets[msg.Platform]; ok {
		rMsg := server.SendNotification(&msg)
		if err2 := json.NewEncoder(w).Encode(rMsg); err2 != nil {
			s.logger.Error("Failed to write message", mlog.Err(err2))
		}
		return
	}
	rMsg := fmt.Sprintf("Did not send message because of missing platform property type=%v serverId=%v", msg.Platform, msg.ServerID)
	s.logger.Error(rMsg)
	resp := NewErrorPushResponse(rMsg)
	err = json.NewEncoder(w).Encode(resp)
	if err != nil {
		s.logger.Error("Failed to write response", mlog.Err(err))
	}
	if s.metrics != nil {
		s.metrics.incrementBadRequest()
	}
}

func (s *Server) handleAckNotification(w http.ResponseWriter, r *http.Request) {
	var ack PushNotificationAck
	err := json.NewDecoder(r.Body).Decode(&ack)
	if err != nil {
		msg := fmt.Sprintf("Failed to read ack body: %v", err)
		s.logger.Error(msg)
		resp := NewErrorPushResponse(msg)
		if err2 := json.NewEncoder(w).Encode(resp); err2 != nil {
			s.logger.Error("Failed to write response", mlog.Err(err2))
		}
		if s.metrics != nil {
			s.metrics.incrementBadRequest()
		}
		return
	}

	if ack.ID == "" {
		msg := "Failed because of missing ack Id"
		s.logger.Error(msg)
		resp := NewErrorPushResponse(msg)
		if err := json.NewEncoder(w).Encode(resp); err != nil {
			s.logger.Error("Failed to write response", mlog.Err(err))
		}
		if s.metrics != nil {
			s.metrics.incrementBadRequest()
		}
		return
	}

	if ack.Platform == "" {
		msg := "Failed because of missing ack platform"
		s.logger.Error(msg)
		resp := NewErrorPushResponse(msg)
		if err := json.NewEncoder(w).Encode(resp); err != nil {
			s.logger.Error("Failed to write response", mlog.Err(err))
		}
		if s.metrics != nil {
			s.metrics.incrementBadRequest()
		}
		return
	}

	if ack.Type == "" {
		msg := "Failed because of missing ack type"
		s.logger.Error(msg)
		resp := NewErrorPushResponse(msg)
		if err := json.NewEncoder(w).Encode(resp); err != nil {
			s.logger.Error("Failed to write response", mlog.Err(err))
		}
		if s.metrics != nil {
			s.metrics.incrementBadRequest()
		}
		return
	}

	// Increment ACK
	s.logger.Info("Acknowledged delivery receipt", mlog.String("ack_id", ack.ID))
	if s.metrics != nil {
		s.metrics.incrementDelivered(ack.Platform, ack.Type)
	}

	rMsg := NewOkPushResponse()
	if err := json.NewEncoder(w).Encode(rMsg); err != nil {
		s.logger.Error("Failed to write message", mlog.Err(err))
	}
}

func (s *Server) getIpAddress(r *http.Request) string {
	address := r.Header.Get(HEADER_FORWARDED)
	var err error

	if address == "" {
		address = r.Header.Get(HEADER_REAL_IP)
	}

	if address == "" {
		address, _, err = net.SplitHostPort(r.RemoteAddr)
		if err != nil {
			s.logger.Error("error in getting IP address", mlog.Err(err))
		}
	}

	return address
}

func getProxyServer() string {
	// HTTPS_PROXY gets the higher priority.
	proxyServer := os.Getenv("HTTPS_PROXY")
	if proxyServer == "" {
		proxyServer = os.Getenv("HTTP_PROXY")
	}
	return proxyServer
}


================================================
FILE: server/server_test.go
================================================
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.

package server

import (
	"bytes"
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"testing"
	"time"

	"github.com/mattermost/mattermost-push-proxy/internal/version"

	"github.com/mattermost/mattermost/server/public/shared/mlog"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

func TestBasicServer(t *testing.T) {
	fileName := FindConfigFile("mattermost-push-proxy.sample.json")
	cfg, err := LoadConfig(fileName)
	require.NoError(t, err)

	logger, err := mlog.NewLogger()
	require.NoError(t, err)

	srv := New(cfg, logger)
	srv.Start()

	time.Sleep(time.Second * 2)

	msg := PushNotification{}
	msg.Message = "test"
	msg.Badge = 1
	msg.DeviceID = "test"

	// Test for missing server Id
	client := http.Client{}
	buf, err := json.Marshal(msg)
	require.NoError(t, err)
	rq, _ := http.NewRequest("POST", "http://localhost:8066/api/v1/send_push", bytes.NewReader(buf))
	if resp, err2 := client.Do(rq); err2 != nil {
		t.Fatal(err2)
	} else {
		pr := PushResponseFromJson(resp.Body)
		if pr == nil || pr[PUSH_STATUS] != PUSH_STATUS_FAIL {
			t.Fatal("invalid response")
		}
	}

	// Test for missing platform type
	msg.ServerID = "test"
	client = http.Client{}
	buf, err = json.Marshal(msg)
	require.NoError(t, err)
	rq, _ = http.NewRequest("POST", "http://localhost:8066/api/v1/send_push", bytes.NewReader(buf))
	if resp, err2 := client.Do(rq); err2 != nil {
		t.Fatal(err2)
	} else {
		pr := PushResponseFromJson(resp.Body)
		if pr == nil || pr[PUSH_STATUS] != PUSH_STATUS_FAIL {
			t.Fatal("invalid response")
		}
	}

	// Test for junk platform type
	msg.Platform = "junk"
	buf, err = json.Marshal(msg)
	require.NoError(t, err)
	rq, _ = http.NewRequest("POST", "http://localhost:8066/api/v1/send_push", bytes.NewReader(buf))
	if resp, err := client.Do(rq); err != nil {
		t.Fatal(err)
	} else {
		pr := PushResponseFromJson(resp.Body)
		if pr == nil || pr[PUSH_STATUS] != PUSH_STATUS_FAIL {
			t.Fatal("invalid response")
		}
	}

	srv.Stop()
	time.Sleep(time.Second * 2)
}

func TestAndroidSend(t *testing.T) {
	fileName := FindConfigFile("mattermost-push-proxy.sample.json")
	cfg, err := LoadConfig(fileName)
	require.NoError(t, err)

	cfg.AndroidPushSettings[0].AndroidAPIKey = "junk"
	logger, err := mlog.NewLogger()
	require.NoError(t, err)

	srv := New(cfg, logger)
	srv.Start()

	time.Sleep(time.Second * 2)

	msg := PushNotification{}
	msg.Message = "test"
	msg.Badge = 1
	msg.Platform = PushNotifyAndroid
	msg.ServerID = "test"
	msg.DeviceID = "test"

	client := http.Client{}
	buf, err := json.Marshal(msg)
	require.NoError(t, err)
	rq, _ := http.NewRequest("POST", "http://localhost:8066/api/v1/send_push", bytes.NewReader(buf))
	if resp, err := client.Do(rq); err != nil {
		t.Fatal(err)
	} else {
		pr := PushResponseFromJson(resp.Body)
		if pr == nil || pr[PUSH_STATUS] != PUSH_STATUS_FAIL {
			t.Fatal("invalid response")
		}
	}

	srv.Stop()
	time.Sleep(time.Second * 2)
}

func TestServer_version(t *testing.T) {
	fileName := FindConfigFile("mattermost-push-proxy.sample.json")
	cfg, err := LoadConfig(fileName)
	require.NoError(t, err)
	logger, err := mlog.NewLogger()
	require.NoError(t, err)

	srv := New(cfg, logger)

	req := httptest.NewRequest(http.MethodGet, "/version", nil)
	res := httptest.NewRecorder()
	srv.version(res, req)
	assert.Equal(t, res.Code, http.StatusOK)

	info := version.VersionInfo()
	ret := struct {
		Version string
		Hash    string
	}{}
	err = json.NewDecoder(res.Body).Decode(&ret)
	assert.NoError(t, err)
	assert.Equal(t, info.BuildVersion, ret.Version)
	assert.Equal(t, info.BuildHash, ret.Hash)
}


================================================
FILE: swagger/spec.yaml
================================================
openapi: 3.0.1
info:
  title: Mattermost Push Proxy
  description: 'This is the OpenAPI documentation for Mattermost Push Proxy REST API'
  version: 1.0.0
servers:
- url: http://url-to-push-proxy.com/api/v1
paths:
  /send_push:
    post:
      summary: Send push notification
      requestBody:
        description: Push notification request body
        content:
          '*/*':
            schema:
              $ref: '#/components/schemas/PushNotification'
            example:
              type: "update_badge"
              device_id: "ackljrfoegdflghdg"
              platform: "android"
              ack_id: "kfs095jsdsdfjslj"
              version: "v2"
              sound: "none"
              cont_ava: 1
              badge: 2
        required: true
      responses:
        default:
          description: response
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: '#/components/schemas/PushResponseOK'
                  - $ref: '#/components/schemas/PushResponseRemove'
                  - $ref: '#/components/schemas/PushResponseError'
              example:
                status: OK
  /ack:
    post:
      summary: Send acknowledgement of a push notification
      requestBody:
        description: Send acknowledgement request body
        content:
          '*/*':
            schema:
              $ref: '#/components/schemas/PushNotificationAck'
            example:
              id: "abcxyz"
              platform: "android"
              type: "message"
        required: true
      responses:
        default:
          description: response
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: '#/components/schemas/PushResponseOK'
                  - $ref: '#/components/schemas/PushResponseError'
              example:
                status: OK
components:
  schemas:
    PushNotification:
      type: object
      properties:
        ack_id:
          type: string
          description: "id of the acknowledgement"
        platform:
          type: string
          description: "type of the platform"
          enum:
          - apple
          - android
          - apple_rn
          - android_rn
        server_id:
          type: string
          description: "id of the server"
        device_id:
          type: string
          description: "id of the device"
        category:
          type: string
          description: "category of the message"
        sound:
          type: string
          description: "type of sound to make when message arrives"
        message:
          type: string
          description: "contents of the message"
        badge:
          type: integer
          description: "badge count"
          format: int64
        cont_ava:
          description: "content available"
          type: integer
          format: int64
        team_id:
          description: "id of the team"
          type: string
        channel_id:
          description: "id of the channel"
          type: string
        root_id:
          description: "id of the root of the post"
          type: string
        post_id:
          description: "id of the post"
          type: string
        channel_name:
          description: "name of the channel"
          type: string
        type:
          type: string
          description: "type of the message"
          enum:
          - message
          - clear
          - update_badge
          - session
        sender_name:
          description: "name of the sender"
          type: string
        sender_id:
          description: "id of the sender"
          type: string
        override_username:
          description: "override the default username"
          type: string
        override_icon_url:
          description: "override the default icon url"
          type: string
        from_webhook:
          description: "name of the webhook"
          type: string
        version:
          type: string
          description: "version of the message"
          default: "v2"
        is_id_loaded:
          description: "whether the message is id_loaded or not"
          type: boolean
    PushNotificationAck:
      type: object
      properties:
        id:
          type: string
          description: "id of the acknowledgement"
        platform:
          type: string
          description: "type of the platform"
          enum:
          - apple
          - android
          - apple_rn
          - android_rn
        type:
          type: string
          description: "type of the message"
          enum:
          - message
          - clear
          - update_badge
          - session
    PushResponseOK:
      type: object
      properties:
        status:
          type: string
          default: "OK"
    PushResponseRemove:
      type: object
      properties:
        status:
          type: string
          default: "REMOVE"
    PushResponseError:
      type: object
      properties:
        status:
          type: string
          default: "FAIL"
        error:
          type: string


================================================
FILE: swagger/template.hbs
================================================
<!DOCTYPE html>
<html>

<head>
  <meta charset="utf8" />
  <title>{{title}}</title>
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <style>
    body {
      padding: 0;
      margin: 0;
    }
  </style>
  {{{redocHead}}}
  {{#unless disableGoogleFont}}<link href="https://fonts.googleapis.com/css?family=Montserrat:300,400,700|Roboto:300,400,700" rel="stylesheet">{{/unless}}
</head>

<body>
  {{{redocHTML}}}
</body>

</html>
Download .txt
gitextract_wfqcrn85/

├── .github/
│   ├── dependabot.yml
│   └── workflows/
│       └── ci.yml
├── .gitignore
├── .golangci.yml
├── CHANGELOG.md
├── CONTRIBUTING.md
├── LICENSE.txt
├── Makefile
├── NOTICE.txt
├── README.md
├── bin/
│   └── .gitignore
├── build/
│   └── setup.txt
├── config/
│   └── mattermost-push-proxy.sample.json
├── docker/
│   ├── Dockerfile
│   ├── Dockerfile.fips
│   └── entrypoint
├── go.mod
├── go.sum
├── internal/
│   └── version/
│       └── version.go
├── main.go
├── package.json
├── scripts/
│   └── go_install.sh
├── server/
│   ├── android_notification_server.go
│   ├── android_notification_test.go
│   ├── apple_notification_server.go
│   ├── config_push_proxy.go
│   ├── logger.go
│   ├── logger_test.go
│   ├── metrics.go
│   ├── metrics_test.go
│   ├── push_notification.go
│   ├── push_response.go
│   ├── server.go
│   └── server_test.go
└── swagger/
    ├── spec.yaml
    └── template.hbs
Download .txt
SYMBOL INDEX (113 symbols across 14 files)

FILE: internal/version/version.go
  function GetVersion (line 30) | func GetVersion() error {
  type Info (line 38) | type Info struct
    method String (line 81) | func (i *Info) String() string {
  function VersionInfo (line 49) | func VersionInfo() Info {

FILE: main.go
  function main (line 22) | func main() {

FILE: server/android_notification_server.go
  constant apnsAuthError (line 26) | apnsAuthError       = "APNS_AUTH_ERROR"
  constant internalError (line 27) | internalError       = "INTERNAL"
  constant thirdPartyAuthError (line 28) | thirdPartyAuthError = "THIRD_PARTY_AUTH_ERROR"
  constant invalidArgument (line 29) | invalidArgument     = "INVALID_ARGUMENT"
  constant quotaExceeded (line 30) | quotaExceeded       = "QUOTA_EXCEEDED"
  constant unregistered (line 31) | unregistered        = "UNREGISTERED"
  constant unavailable (line 32) | unavailable         = "UNAVAILABLE"
  constant tokenSourceError (line 33) | tokenSourceError    = "TOKEN_SOURCE_ERROR"
  constant scope (line 37) | scope = "https://www.googleapis.com/auth/firebase.messaging"
  type AndroidNotificationServer (line 40) | type AndroidNotificationServer struct
    method Initialize (line 70) | func (me *AndroidNotificationServer) Initialize() error {
    method SendNotification (line 116) | func (me *AndroidNotificationServer) SendNotification(msg *PushNotific...
    method SendNotificationWithRetry (line 237) | func (me *AndroidNotificationServer) SendNotificationWithRetry(fcmMsg ...
  type serviceAccount (line 51) | type serviceAccount struct
  function NewAndroidNotificationServer (line 60) | func NewAndroidNotificationServer(settings AndroidPushSettings, logger *...
  function isRetryable (line 292) | func isRetryable(err error) bool {
  function getErrorCode (line 309) | func getErrorCode(err error) (string, bool) {

FILE: server/android_notification_test.go
  function TestAndroidInitialize (line 18) | func TestAndroidInitialize(t *testing.T) {
  type ErrorCode (line 58) | type ErrorCode
  type FirebaseError (line 59) | type FirebaseError struct
    method Error (line 66) | func (fe *FirebaseError) Error() string {
  function TestGetErrorCode (line 70) | func TestGetErrorCode(t *testing.T) {

FILE: server/apple_notification_server.go
  type AppleNotificationServer (line 24) | type AppleNotificationServer struct
    method setupProxySettings (line 43) | func (me *AppleNotificationServer) setupProxySettings(appleCert *tls.C...
    method Initialize (line 77) | func (me *AppleNotificationServer) Initialize() error {
    method SendNotification (line 119) | func (me *AppleNotificationServer) SendNotification(msg *PushNotificat...
    method SendNotificationWithRetry (line 293) | func (me *AppleNotificationServer) SendNotificationWithRetry(notificat...
  function NewAppleNotificationServer (line 33) | func NewAppleNotificationServer(settings ApplePushSettings, logger *mlog...

FILE: server/config_push_proxy.go
  type ConfigPushProxy (line 14) | type ConfigPushProxy struct
  type ApplePushSettings (line 30) | type ApplePushSettings struct
  type AndroidPushSettings (line 41) | type AndroidPushSettings struct
  function FindConfigFile (line 49) | func FindConfigFile(fileName string) string {
  function LoadConfig (line 64) | func LoadConfig(fileName string) (*ConfigPushProxy, error) {

FILE: server/logger.go
  function NewLogger (line 11) | func NewLogger(cfg *ConfigPushProxy) (*mlog.Logger, error) {
  function buildLogConfig (line 28) | func buildLogConfig(cfg *ConfigPushProxy) mlog.LoggerConfiguration {
  function buildConsoleLogConfig (line 42) | func buildConsoleLogConfig(format string) mlog.TargetCfg {
  function buildLogFileConfig (line 53) | func buildLogFileConfig(filename string, format string) mlog.TargetCfg {

FILE: server/logger_test.go
  function TestNewMlogLogger (line 13) | func TestNewMlogLogger(t *testing.T) {

FILE: server/metrics.go
  constant metricNotificationsTotalName (line 11) | metricNotificationsTotalName   = "service_notifications_total"
  constant metricSuccessName (line 12) | metricSuccessName              = "service_success_total"
  constant metricSuccessWithAckName (line 13) | metricSuccessWithAckName       = "service_success_with_ack_total"
  constant metricDeliveredName (line 14) | metricDeliveredName            = "service_delivered_total"
  constant metricFailureName (line 15) | metricFailureName              = "service_failure_total"
  constant metricFailureWithReasonName (line 16) | metricFailureWithReasonName    = "service_failure_with_reason_total"
  constant metricRemovalName (line 17) | metricRemovalName              = "service_removal_total"
  constant metricBadRequestName (line 18) | metricBadRequestName           = "service_bad_request_total"
  constant metricFCMResponseName (line 19) | metricFCMResponseName          = "service_fcm_request_duration_seconds"
  constant metricAPNSResponseName (line 20) | metricAPNSResponseName         = "service_apns_request_duration_seconds"
  constant metricServiceResponseName (line 21) | metricServiceResponseName      = "service_request_duration_seconds"
  constant metricNotificationResponseName (line 22) | metricNotificationResponseName = "service_notification_duration_seconds"
  function NewPrometheusHandler (line 26) | func NewPrometheusHandler() http.Handler {
  type metrics (line 30) | type metrics struct
    method shutdown (line 117) | func (m *metrics) shutdown() {
    method incrementNotificationTotal (line 137) | func (m *metrics) incrementNotificationTotal(platform, pushType string) {
    method incrementSuccess (line 141) | func (m *metrics) incrementSuccess(platform, pushType string) {
    method incrementSuccessWithAck (line 145) | func (m *metrics) incrementSuccessWithAck(platform, pushType string) {
    method incrementDelivered (line 150) | func (m *metrics) incrementDelivered(platform, pushType string) {
    method incrementFailure (line 154) | func (m *metrics) incrementFailure(platform, pushType, reason string) {
    method incrementRemoval (line 161) | func (m *metrics) incrementRemoval(platform, pushType, reason string) {
    method incrementBadRequest (line 166) | func (m *metrics) incrementBadRequest() {
    method observeAPNSResponse (line 170) | func (m *metrics) observeAPNSResponse(dur float64) {
    method observeFCMResponse (line 174) | func (m *metrics) observeFCMResponse(dur float64) {
    method observeServiceResponse (line 178) | func (m *metrics) observeServiceResponse(dur float64) {
    method observerNotificationResponse (line 182) | func (m *metrics) observerNotificationResponse(platform string, dur fl...
  function newMetrics (line 46) | func newMetrics() *metrics {

FILE: server/metrics_test.go
  function TestMetricDisabled (line 16) | func TestMetricDisabled(t *testing.T) {
  function TestMetricEnabled (line 66) | func TestMetricEnabled(t *testing.T) {

FILE: server/push_notification.go
  constant PushNotifyApple (line 7) | PushNotifyApple   = "apple"
  constant PushNotifyAndroid (line 8) | PushNotifyAndroid = "android"
  constant PushTypeMessage (line 10) | PushTypeMessage     = "message"
  constant PushTypeClear (line 11) | PushTypeClear       = "clear"
  constant PushTypeUpdateBadge (line 12) | PushTypeUpdateBadge = "update_badge"
  constant PushTypeSession (line 13) | PushTypeSession     = "session"
  constant PushTypeTest (line 14) | PushTypeTest        = "test"
  constant PushMessageV2 (line 16) | PushMessageV2 = "v2"
  constant PushSoundNone (line 18) | PushSoundNone = "none"
  type PushNotificationAck (line 21) | type PushNotificationAck struct
  type PushNotification (line 27) | type PushNotification struct

FILE: server/push_response.go
  constant PUSH_STATUS (line 12) | PUSH_STATUS           = "status"
  constant PUSH_STATUS_OK (line 13) | PUSH_STATUS_OK        = "OK"
  constant PUSH_STATUS_FAIL (line 14) | PUSH_STATUS_FAIL      = "FAIL"
  constant PUSH_STATUS_REMOVE (line 15) | PUSH_STATUS_REMOVE    = "REMOVE"
  constant PUSH_STATUS_ERROR_MSG (line 16) | PUSH_STATUS_ERROR_MSG = "error"
  type PushResponse (line 19) | type PushResponse
  function NewOkPushResponse (line 21) | func NewOkPushResponse() PushResponse {
  function NewRemovePushResponse (line 27) | func NewRemovePushResponse() PushResponse {
  function NewErrorPushResponse (line 33) | func NewErrorPushResponse(message string) PushResponse {
  function PushResponseFromJson (line 40) | func PushResponseFromJson(data io.Reader) PushResponse {

FILE: server/server.go
  constant HEADER_FORWARDED (line 27) | HEADER_FORWARDED           = "X-Forwarded-For"
  constant HEADER_REAL_IP (line 28) | HEADER_REAL_IP             = "X-Real-IP"
  constant WAIT_FOR_SERVER_SHUTDOWN (line 29) | WAIT_FOR_SERVER_SHUTDOWN   = time.Second * 5
  constant CONNECTION_TIMEOUT_SECONDS (line 30) | CONNECTION_TIMEOUT_SECONDS = 60
  constant MAX_RETRIES (line 31) | MAX_RETRIES                = 3
  type NotificationServer (line 34) | type NotificationServer interface
  type Server (line 40) | type Server struct
    method Start (line 58) | func (s *Server) Start() {
    method Stop (line 138) | func (s *Server) Stop() {
    method version (line 156) | func (s *Server) version(w http.ResponseWriter, _ *http.Request) {
    method responseTimeMiddleware (line 168) | func (s *Server) responseTimeMiddleware(f func(w http.ResponseWriter, ...
    method handleSendNotification (line 178) | func (s *Server) handleSendNotification(w http.ResponseWriter, r *http...
    method handleAckNotification (line 263) | func (s *Server) handleAckNotification(w http.ResponseWriter, r *http....
    method getIpAddress (line 330) | func (s *Server) getIpAddress(r *http.Request) string {
  function New (line 49) | func New(cfg *ConfigPushProxy, logger *mlog.Logger) *Server {
  function root (line 152) | func root(w http.ResponseWriter, r *http.Request) {
  function getProxyServer (line 348) | func getProxyServer() string {

FILE: server/server_test.go
  function TestBasicServer (line 21) | func TestBasicServer(t *testing.T) {
  function TestAndroidSend (line 86) | func TestAndroidSend(t *testing.T) {
  function TestServer_version (line 124) | func TestServer_version(t *testing.T) {
Condensed preview — 36 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (201K chars).
[
  {
    "path": ".github/dependabot.yml",
    "chars": 405,
    "preview": "version: 2\nupdates:\n  - package-ecosystem: \"github-actions\"\n    directory: \"/\"\n    reviewers:\n      - \"mattermost/cloud-"
  },
  {
    "path": ".github/workflows/ci.yml",
    "chars": 25273,
    "preview": "name: Build and Test\n\non:\n  push:\n    branches:\n      - master\n  pull_request:\n\nconcurrency:\n  group: ${{ github.workflo"
  },
  {
    "path": ".gitignore",
    "chars": 790,
    "preview": "\nlogs\n.DS_Store\ndist\n\n# Build Targets\n.prebuild\n.prebuild-go\n.prebuild-jsx\n\n# Compiled Object files, Static and Dynamic "
  },
  {
    "path": ".golangci.yml",
    "chars": 379,
    "preview": "# options for analysis running\nrun:\n  # timeout for analysis, e.g. 30s, 5m, default is 1m\n  timeout: 5m\n\nlinters-setting"
  },
  {
    "path": "CHANGELOG.md",
    "chars": 1298,
    "preview": "# Mattermost Push Proxy Changelog\n\n\n## 5.8.1 Release\n- Release Date: March 28, 2019\n- Compatible with all versions of Ma"
  },
  {
    "path": "CONTRIBUTING.md",
    "chars": 88,
    "preview": "Please see: https://github.com/mattermost/mattermost-server/blob/master/CONTRIBUTING.md\n"
  },
  {
    "path": "LICENSE.txt",
    "chars": 11413,
    "preview": "Copyright (c) 2015 Mattermost, Inc. All rights reserved.\n\n                               Apache License\n                "
  },
  {
    "path": "Makefile",
    "chars": 34091,
    "preview": "# ====================================================================================\n# Variables\n\n## General Variables"
  },
  {
    "path": "NOTICE.txt",
    "chars": 129,
    "preview": "Mattermost Platform \n© 2015 Mattermost, Inc.  All Rights Reserved.  See LICENSE.txt for license information.\n\nNOTICES: \n"
  },
  {
    "path": "README.md",
    "chars": 673,
    "preview": "# Mattermost Push Proxy ![CircleCI branch](https://img.shields.io/circleci/project/github/mattermost/mattermost-push-pro"
  },
  {
    "path": "bin/.gitignore",
    "chars": 69,
    "preview": "# Ignore all files in this directory\n*\n# Except this one\n!.gitignore\n"
  },
  {
    "path": "build/setup.txt",
    "chars": 632,
    "preview": "#!/bin/bash\n\n# copy following lines to /etc/init/matter-push-proxy.conf\n\n# start on runlevel [2345]\n# stop on runlevel ["
  },
  {
    "path": "config/mattermost-push-proxy.sample.json",
    "chars": 1245,
    "preview": "{\n    \"ListenAddress\":\":8066\",\n    \"ThrottlePerSec\":300,\n    \"ThrottleMemoryStoreSize\":50000,\n    \"ThrottleVaryByHeader\""
  },
  {
    "path": "docker/Dockerfile",
    "chars": 1200,
    "preview": "FROM --platform=${TARGETPLATFORM} ubuntu:noble-20250529 AS builder\nARG TARGETARCH\n\n# Setting bash as our shell, and enab"
  },
  {
    "path": "docker/Dockerfile.fips",
    "chars": 1713,
    "preview": "# Build the mattermost-push-proxy (FIPS version)\nARG BUILD_IMAGE=cgr.dev/mattermost.com/go-msft-fips:1.24.6\nARG BASE_IMA"
  },
  {
    "path": "docker/entrypoint",
    "chars": 36,
    "preview": "#!/bin/sh -e\n\nexec ${PUSH_PROXY} $@\n"
  },
  {
    "path": "go.mod",
    "chars": 4519,
    "preview": "module github.com/mattermost/mattermost-push-proxy\n\ngo 1.24.6\n\nrequire (\n\tfirebase.google.com/go/v4 v4.18.0\n\tgithub.com/"
  },
  {
    "path": "go.sum",
    "chars": 39480,
    "preview": "cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY=\ncel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX"
  },
  {
    "path": "internal/version/version.go",
    "chars": 2647,
    "preview": "package version\n\nimport (\n\t\"fmt\"\n\t\"runtime\"\n\t\"strings\"\n\t\"text/tabwriter\"\n)\n\n// Base version information.\n//\n// This is t"
  },
  {
    "path": "main.go",
    "chars": 1281,
    "preview": "// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.\n// See License.txt for license information.\n\npackage main\n\ni"
  },
  {
    "path": "package.json",
    "chars": 918,
    "preview": "{\n  \"name\": \"mattermost-push-proxy\",\n  \"version\": \"1.0.0\",\n  \"description\": \"Repository for mattermost push proxy\",\n  \"m"
  },
  {
    "path": "scripts/go_install.sh",
    "chars": 958,
    "preview": "#!/usr/bin/env bash\n\n# Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.\n# See LICENSE.txt for license in"
  },
  {
    "path": "server/android_notification_server.go",
    "chars": 9018,
    "preview": "// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.\n// See License.txt for license information.\n\npackage server\n"
  },
  {
    "path": "server/android_notification_test.go",
    "chars": 2307,
    "preview": "// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.\n// See License.txt for license information.\n\npackage server\n"
  },
  {
    "path": "server/apple_notification_server.go",
    "chars": 9795,
    "preview": "// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.\n// See License.txt for license information.\n\npackage server\n"
  },
  {
    "path": "server/config_push_proxy.go",
    "chars": 3156,
    "preview": "// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.\n// See License.txt for license information.\n\npackage server\n"
  },
  {
    "path": "server/logger.go",
    "chars": 2016,
    "preview": "// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.\n// See License.txt for license information.\n\npackage server\n"
  },
  {
    "path": "server/logger_test.go",
    "chars": 1359,
    "preview": "// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.\n// See License.txt for license information.\n\npackage server\n"
  },
  {
    "path": "server/metrics.go",
    "chars": 6272,
    "preview": "package server\n\nimport (\n\t\"net/http\"\n\n\t\"github.com/prometheus/client_golang/prometheus\"\n\t\"github.com/prometheus/client_g"
  },
  {
    "path": "server/metrics_test.go",
    "chars": 3826,
    "preview": "package server\n\nimport (\n\t\"io\"\n\t\"net/http\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/prometheus/common/expfmt\"\n\t\"githu"
  },
  {
    "path": "server/push_notification.go",
    "chars": 1777,
    "preview": "// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.\n// See License.txt for license information.\n\npackage server\n"
  },
  {
    "path": "server/push_response.go",
    "chars": 1008,
    "preview": "// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.\n// See License.txt for license information.\n\npackage server\n"
  },
  {
    "path": "server/server.go",
    "chars": 10057,
    "preview": "// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.\n// See License.txt for license information.\n\npackage server\n"
  },
  {
    "path": "server/server_test.go",
    "chars": 3682,
    "preview": "// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.\n// See License.txt for license information.\n\npackage server\n"
  },
  {
    "path": "swagger/spec.yaml",
    "chars": 5166,
    "preview": "openapi: 3.0.1\ninfo:\n  title: Mattermost Push Proxy\n  description: 'This is the OpenAPI documentation for Mattermost Pus"
  },
  {
    "path": "swagger/template.hbs",
    "chars": 451,
    "preview": "<!DOCTYPE html>\n<html>\n\n<head>\n  <meta charset=\"utf8\" />\n  <title>{{title}}</title>\n  <meta name=\"viewport\" content=\"wid"
  }
]

About this extraction

This page contains the full source code of the mattermost/mattermost-push-proxy GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 36 files (184.7 KB), approximately 62.6k tokens, and a symbol index with 113 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!