diff --git a/.github/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md
deleted file mode 100644
index c788100e..00000000
--- a/.github/CODE_OF_CONDUCT.md
+++ /dev/null
@@ -1,76 +0,0 @@
-# Contributor Code of Conduct
-
-## Our Pledge
-
-In the interest of fostering an open and welcoming environment, we as
-contributors and maintainers pledge to make participation in our project and
-our community a harassment-free experience for everyone, regardless of age, body
-size, disability, ethnicity, sex characteristics, gender identity and expression,
-level of experience, education, socio-economic status, nationality, personal
-appearance, ethnicity, religion, or sexual identity and orientation.
-
-## Our Standards
-
-Examples of behavior that contributes to creating a positive environment
-include:
-
-* Using welcoming and inclusive language
-* Being respectful of differing viewpoints and experiences
-* Gracefully accepting constructive criticism
-* Focusing on what is best for the community
-* Showing empathy towards other community members
-
-Examples of unacceptable behavior by participants include:
-
-* The use of sexualized language or imagery and unwelcome sexual attention or
- advances
-* Trolling, insulting/derogatory comments, and personal or political attacks
-* Public or private harassment
-* Publishing others' private information, such as a physical or electronic
- address, without explicit permission
-* Other conduct which could reasonably be considered inappropriate in a
- professional setting
-
-## Our Responsibilities
-
-Project maintainers are responsible for clarifying the standards of acceptable
-behavior and are expected to take appropriate and fair corrective action in
-response to any instances of unacceptable behavior.
-
-Project maintainers have the right and responsibility to remove, edit, or
-reject comments, commits, code, wiki edits, issues, and other contributions
-that are not aligned to this Code of Conduct, or to ban temporarily or
-permanently any contributor for other behaviors that they deem inappropriate,
-threatening, offensive, or harmful.
-
-## Scope
-
-This Code of Conduct applies within all project spaces, and it also applies when
-an individual is representing the project or its community in public spaces.
-Examples of representing a project or community include using an official
-project e-mail address, posting via an official social media account, or acting
-as an appointed representative at an online or offline event. Representation of
-a project may be further defined and clarified by project maintainers.
-
-## Enforcement
-
-Instances of abusive, harassing, or otherwise unacceptable behavior may be
-reported by contacting the project team at contact+coc@mercure.rocks. All
-complaints will be reviewed and investigated and will result in a response that
-is deemed necessary and appropriate to the circumstances. The project team is
-obligated to maintain confidentiality with regard to the reporter of an incident.
-Further details of specific enforcement policies may be posted separately.
-
-Project maintainers who do not follow or enforce the Code of Conduct in good
-faith may face temporary or permanent repercussions as determined by other
-members of the project's leadership.
-
-## Attribution
-
-This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
-available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
-
-[homepage]: https://www.contributor-covenant.org
-
-For answers to common questions about this code of conduct, see
-https://www.contributor-covenant.org/faq
diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
deleted file mode 100644
index 3df6d044..00000000
--- a/.github/FUNDING.yml
+++ /dev/null
@@ -1 +0,0 @@
-github: dunglas
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 00000000..e993f44f
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,9 @@
+version: 2
+updates:
+ -
+ package-ecosystem: github-actions
+ directory: /
+ schedule:
+ interval: weekly
+ commit-message:
+ prefix: ci
diff --git a/.github/linters/.htmlhintrc b/.github/linters/.htmlhintrc
new file mode 100644
index 00000000..c3f4a5d5
--- /dev/null
+++ b/.github/linters/.htmlhintrc
@@ -0,0 +1,25 @@
+{
+ "tagname-lowercase": true,
+ "attr-lowercase": true,
+ "attr-value-double-quotes": true,
+ "attr-value-not-empty": false,
+ "attr-no-duplication": true,
+ "doctype-first": false,
+ "tag-pair": true,
+ "tag-self-close": false,
+ "spec-char-escape": true,
+ "id-unique": true,
+ "src-not-empty": true,
+ "title-require": true,
+ "alt-require": true,
+ "doctype-html5": true,
+ "id-class-value": false,
+ "style-disabled": false,
+ "inline-style-disabled": false,
+ "inline-script-disabled": false,
+ "space-tab-mixed-disabled": "space",
+ "id-class-ad-disabled": false,
+ "href-abs-or-rel": false,
+ "attr-unsafe-chars": true,
+ "head-script-disabled": true
+}
diff --git a/.github/linters/.jscpd.json b/.github/linters/.jscpd.json
new file mode 100644
index 00000000..1f46b2e4
--- /dev/null
+++ b/.github/linters/.jscpd.json
@@ -0,0 +1,13 @@
+{
+ "threshold": 0,
+ "reporters": ["consoleFull"],
+ "ignore": [
+ "**/__snapshots__/**",
+ "**/*_test.go",
+ "**/examples",
+ "**/node_modules",
+ "**/dist",
+ "**/.goreleaser.yml"
+ ],
+ "absolute": true
+}
diff --git a/.github/linters/.markdown-lint.yml b/.github/linters/.markdown-lint.yml
new file mode 100644
index 00000000..f10f13af
--- /dev/null
+++ b/.github/linters/.markdown-lint.yml
@@ -0,0 +1,36 @@
+---
+###########################
+###########################
+## Markdown Linter rules ##
+###########################
+###########################
+
+# Linter rules doc:
+# - https://github.com/DavidAnson/markdownlint
+#
+# Note:
+# To comment out a single error:
+#
+# any violations you want
+#
+#
+
+###############
+# Rules by id #
+###############
+MD004: false # Unordered list style
+MD007:
+ indent: 2 # Unordered list indentation
+MD010:
+ ignore_code_languages: [caddyfile]
+MD013: false
+MD026:
+ punctuation: ".,;:!。,;:" # List of not allowed
+MD029: false # Ordered list item prefix
+MD033: false # Allow inline HTML
+MD036: false # Emphasis used instead of a heading
+
+#################
+# Rules by tags #
+#################
+blank_lines: false # Error on blank lines
diff --git a/.github/linters/.textlintrc b/.github/linters/.textlintrc
new file mode 100644
index 00000000..bd70fc22
--- /dev/null
+++ b/.github/linters/.textlintrc
@@ -0,0 +1,13 @@
+{
+ "filters": {
+ "comments": true
+ },
+ "rules": {
+ "terminology": {
+ "defaultTerms": true,
+ "exclude": [
+ "(?<=(?:\\w+[^.?!])? )internet\\b(?! explorer)"
+ ]
+ }
+ }
+}
diff --git a/.github/linters/.yaml-lint.yml b/.github/linters/.yaml-lint.yml
new file mode 100644
index 00000000..21be47b2
--- /dev/null
+++ b/.github/linters/.yaml-lint.yml
@@ -0,0 +1,9 @@
+---
+extends: relaxed
+
+rules:
+ line-length: disable
+
+ignore: |
+ charts/
+ chart/
diff --git a/.github/workflows/cd-chart.yml b/.github/workflows/cd-chart.yml
index 38c71750..332feae2 100644
--- a/.github/workflows/cd-chart.yml
+++ b/.github/workflows/cd-chart.yml
@@ -10,7 +10,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
- uses: actions/checkout@v2
+ uses: actions/checkout@v4
with:
fetch-depth: 0
@@ -20,14 +20,11 @@ jobs:
git config user.email "$GITHUB_ACTOR@users.noreply.github.com"
- name: Install Helm
- uses: azure/setup-helm@v1
- with:
- version: v3.4.0
+ uses: azure/setup-helm@v4
- name: Run chart-releaser
uses: dunglas/chart-releaser-action@feat/match-tags
with:
- charts_repo_url: https://charts.mercure.rocks
match_tags: helm-chart-*
env:
CR_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
diff --git a/.github/workflows/cd-docs.yml b/.github/workflows/cd-docs.yml
new file mode 100644
index 00000000..c5ef6f13
--- /dev/null
+++ b/.github/workflows/cd-docs.yml
@@ -0,0 +1,23 @@
+name: Publish Docs
+
+on:
+ push:
+ branches:
+ - main
+ paths:
+ - docs/**
+
+jobs:
+ publish:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 2
+
+ - name: Revalidate changed pages
+ run: |
+ shopt -s globstar
+ FILES=$(git --no-pager diff --name-only HEAD~1 -- docs/**/*.md | awk 'BEGIN{RS="\n"} {printf "%s%s",sep,$0; sep="&files="}')
+ curl --silent --fail-with-body "https://mercure.rocks/api/revalidate?secret=${{ secrets.REVALIDATE_TOKEN }}&files=$FILES"
diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml
index 3df444b5..caaa790b 100644
--- a/.github/workflows/cd.yml
+++ b/.github/workflows/cd.yml
@@ -1,35 +1,66 @@
name: Release Hub
on:
+ pull_request:
+ branches:
+ - main
push:
+ branches:
+ - main
tags:
- - 'v*'
+ - v*.*.*
+ workflow_dispatch:
+ inputs: {}
-permissions: write-all
+permissions:
+ contents: write
jobs:
release:
- name: Release
runs-on: ubuntu-latest
steps:
- name: Checkout
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
with:
fetch-depth: 0
+
- name: Set up Go
- uses: actions/setup-go@v4
+ uses: actions/setup-go@v5
with:
- go-version: '1.17'
+ go-version: '1.22'
+ cache-dependency-path: |
+ go.sum
+ caddy/go.sum
+
- name: Login to GitHub Container Registry
- uses: docker/login-action@v2
+ if: startsWith(github.ref, 'refs/tags/v')
+ uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Set GoReleaser Git tags
+ run: |
+ tags=$(git tag --list --sort=-version:refname 'v*')
+ echo "GORELEASER_CURRENT_TAG=$(awk 'NR==1 {print;exit}' <<< "$tags")" >> "$GITHUB_ENV"
+ echo "GORELEASER_PREVIOUS_TAG=$(awk 'NR==2 {print;exit}' <<< "$tags")" >> "$GITHUB_ENV"
+
- name: Run GoReleaser
- uses: goreleaser/goreleaser-action@v4
+ uses: goreleaser/goreleaser-action@v5
with:
version: latest
- args: release --clean
+ args: release --clean --timeout 60m ${{ !startsWith(github.ref, 'refs/tags/v') && '--snapshot --skip=sign' || '' }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Display version
+ run: dist/caddy_linux_amd64_v1/mercure version
+
+ - name: Upload snapshot
+ if: ${{ !startsWith(github.ref, 'refs/tags/v') }}
+ uses: actions/upload-artifact@v4
+ with:
+ name: snapshot
+ path: dist/*
+ retention-days: 1
diff --git a/.github/workflows/ci-chart.yml b/.github/workflows/ci-chart.yml
index 9dc7c9c4..541f2613 100644
--- a/.github/workflows/ci-chart.yml
+++ b/.github/workflows/ci-chart.yml
@@ -1,6 +1,14 @@
name: Lint and Test Chart
-on: pull_request
+on:
+ pull_request:
+ branches:
+ - main
+ push:
+ branches:
+ - main
+ tags:
+ - v*.*.*
jobs:
lint-test:
@@ -9,36 +17,36 @@ jobs:
CT_TARGET_BRANCH: main
steps:
- name: Checkout
- uses: actions/checkout@v2
+ uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Helm
- uses: azure/setup-helm@v1
- with:
- version: v3.4.0
+ uses: azure/setup-helm@v4
- - uses: actions/setup-python@v2
+ - uses: actions/setup-python@v5
with:
- python-version: 3.7
+ python-version: '3.10'
- name: Set up chart-testing
- uses: helm/chart-testing-action@v2.1.0
+ uses: helm/chart-testing-action@v2
- name: Run chart-testing (list-changed)
id: list-changed
run: |
- changed=$(ct list-changed)
+ changed=$(ct list-changed --target-branch ${{ github.event.repository.default_branch }})
if [[ -n "$changed" ]]; then
- echo "::set-output name=changed::true"
+ echo "changed=true" >> "$GITHUB_OUTPUT"
fi
- name: Run chart-testing (lint)
- run: ct lint --check-version-increment=false
+ if: steps.list-changed.outputs.changed == 'true'
+ run: ct lint --check-version-increment=false --target-branch ${{ github.event.repository.default_branch }}
- name: Create kind cluster
- uses: helm/kind-action@v1.2.0
if: steps.list-changed.outputs.changed == 'true'
+ uses: helm/kind-action@v1
- name: Run chart-testing (install)
- run: ct install
+ if: steps.list-changed.outputs.changed == 'true'
+ run: ct install --target-branch ${{ github.event.repository.default_branch }}
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index b638ca3e..3e48d873 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -1,8 +1,14 @@
name: Lint and Test Hub
on:
- push:
pull_request:
+ branches:
+ - main
+ push:
+ branches:
+ - main
+ tags:
+ - v*.*.*
env:
GO111MODULE: 'on'
@@ -12,50 +18,88 @@ jobs:
name: Lint
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v2
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - uses: actions/setup-go@v5
+ with:
+ go-version: '1.22'
+ cache-dependency-path: |
+ go.sum
+ caddy/go.sum
- name: golangci-lint
- uses: golangci/golangci-lint-action@v2
+ uses: golangci/golangci-lint-action@v4
with:
version: latest
+ args: --timeout=30m
test:
+ strategy:
+ matrix:
+ go: [ '1.21', '1.22' ]
+ fail-fast: false
name: Test
runs-on: ubuntu-latest
+ env:
+ # Temporary workaround for https://github.com/golang/go/issues/65653
+ GOEXPERIMENT: nocoverageredesign
steps:
- - name: Checkout
- uses: actions/checkout@v2
-
- - name: Set up Go
- uses: actions/setup-go@v2
- with:
- go-version: '1.17'
-
- - uses: actions/cache@v2
- with:
- path: |
- ~/go/pkg/mod # Module download cache
- ~/.cache/go-build # Build cache (Linux)
- key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
- restore-keys: |
- ${{ runner.os }}-go-
-
- - name: Install project dependencies
- run: go get
-
- - name: Use go-deadlock
- run: ./tests/use-go-deadlock.sh
-
- - name: Test
- run: go test -race -covermode atomic -coverprofile=profile.cov -coverpkg=github.com/dunglas/mercure ./...
-
- - name: Test Caddy module
- run: |
- go test -timeout 1m -race -covermode atomic -coverprofile=profile.cov -coverpkg=github.com/dunglas/mercure ./...
- sed '1d' profile.cov >> ../profile.cov
- working-directory: ./caddy
-
- - name: Upload coverage results
- uses: shogo82148/actions-goveralls@v1
- with:
- path-to-profile: profile.cov
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Set up Go
+ uses: actions/setup-go@v5
+ with:
+ go-version: ${{ matrix.go }}
+ cache-dependency-path: |
+ go.sum
+ caddy/go.sum
+
+ - name: Use go-deadlock
+ run: ./tests/use-go-deadlock.sh
+
+ - name: Test
+ run: go test -race -covermode atomic -coverprofile=profile.cov -coverpkg=github.com/dunglas/mercure ./...
+
+ - name: Test Caddy module
+ working-directory: caddy/
+ run: |
+ go test -timeout 1m -race -covermode atomic -coverprofile=profile.cov -coverpkg=github.com/dunglas/mercure ./...
+ sed '1d' profile.cov >> ../profile.cov
+
+ - name: Upload coverage results
+ uses: shogo82148/actions-goveralls@v1
+ with:
+ path-to-profile: profile.cov
+ parallel: true
+
+ - name: Start Mercure
+ working-directory: caddy/mercure/
+ run: sudo MERCURE_PUBLISHER_JWT_KEY='!ChangeThisMercureHubJWTSecretKey!' MERCURE_SUBSCRIBER_JWT_KEY='!ChangeThisMercureHubJWTSecretKey!' go run main.go start --config ../../Caddyfile.dev
+
+ - uses: actions/setup-node@v4
+ with:
+ node-version: '16'
+ cache: 'npm'
+ cache-dependency-path: conformance-tests/package-lock.json
+
+ - name: Install Playwrigth dependencies
+ working-directory: conformance-tests/
+ run: npm ci
+
+ - name: Install playwright browsers
+ working-directory: conformance-tests/
+ run: npx playwright install --with-deps
+
+ - name: Run Playwright tests
+ working-directory: conformance-tests/
+ run: npx playwright test
+
+ finish:
+ needs: test
+ runs-on: ubuntu-latest
+ steps:
+ - uses: shogo82148/actions-goveralls@v1
+ with:
+ parallel-finished: true
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
new file mode 100644
index 00000000..36318d92
--- /dev/null
+++ b/.github/workflows/lint.yml
@@ -0,0 +1,31 @@
+name: Lint Docs and Non-Go Code
+
+on:
+ pull_request:
+ branches:
+ - main
+ push:
+ branches:
+ - main
+ tags:
+ - v*.*.*
+
+jobs:
+ lint:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+ - name: Lint Code Base
+ uses: super-linter/super-linter/slim@v5
+ env:
+ VALIDATE_ALL_CODEBASE: true
+ VALIDATE_GITLEAKS: false
+ VALIDATE_GO: false
+ VALIDATE_PHP_PHPCS: false
+ VALIDATE_KUBERNETES_KUBECONFORM: false
+ VALIDATE_KUBERNETES_KUBEVAL: false
+ VALIDATE_TYPESCRIPT_STANDARD: false
+ DEFAULT_BRANCH: main
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.gitignore b/.gitignore
index abcfc573..9276675e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,6 +4,7 @@ __pycache__
dist
/caddy/caddy
/caddy/mercure/mercure
+/caddy/mercure/__debug_bin
/cmd/mercure/mercure
/certs
/spec/mercure.html
diff --git a/.golangci.yml b/.golangci.yml
index 28401d1e..47948b04 100644
--- a/.golangci.yml
+++ b/.golangci.yml
@@ -10,18 +10,30 @@ linters:
- wsl
- gomnd
- testpackage
- - exhaustivestruct
+ - exhaustruct
- paralleltest
- cyclop
- forcetypeassert
- tagliatelle
- varnamelen
+ - nonamedreturns
+ - testableexamples
+ - musttag
+ - depguard
+ # Weird issues
+ - nolintlint
# deprecated
- interfacer
- maligned
- scopelint
- golint
+ - exhaustivestruct
+ - varcheck
+ - ifshort
+ - deadcode
+ - nosnakecase
+ - structcheck
issues:
exclude-rules:
@@ -32,3 +44,4 @@ issues:
- godox
- noctx
- wrapcheck
+ - goconst
diff --git a/.goreleaser.yml b/.goreleaser.yml
index c0c4d1da..a43944ff 100644
--- a/.goreleaser.yml
+++ b/.goreleaser.yml
@@ -1,104 +1,122 @@
before:
hooks:
- - go mod download
+ - go mod tidy
checksum:
- name_template: 'checksums.txt'
+ name_template: "checksums.txt"
snapshot:
- name_template: "{{ .Tag }}-next"
+ name_template: "{{ incpatch .Version }}-next"
changelog:
sort: asc
filters:
exclude:
- - '^docs:'
- - '^test:'
- - '^ci:'
- - '^build:'
- - '^chore:'
- - '^Merge'
+ - "^docs:"
+ - "^test:"
+ - "^ci:"
+ - "^build:"
+ - "^chore:"
+ - "^Merge"
release:
prerelease: auto
env:
- CGO_ENABLED=0
builds:
-- id: caddy
- dir: caddy/mercure
- goos:
- - linux
- - darwin
- - windows
- goarch:
- - 386
- - amd64
- - arm
- - arm64
- goarm:
- - 5
- - 6
- - 7
+ - id: caddy
+ dir: caddy/mercure
+ ldflags:
+ - -X 'github.com/caddyserver/caddy/v2.CustomVersion=Mercure.rocks {{ .Version }} Caddy'
+ goos:
+ - linux
+ - darwin
+ - windows
+ goarch:
+ - "386"
+ - amd64
+ - arm
+ - arm64
+ goarm:
+ - "5"
+ - "6"
+ - "7"
+upx:
+ - enabled: true
+ goos: [linux]
+ compress: best
archives:
-- builds:
- - caddy
- name_template: >-
- {{ .ProjectName }}_
- {{- title .Os }}_
- {{- if eq .Arch "amd64" }}x86_64
- {{- else if eq .Arch "386" }}i386
- {{- else }}{{ .Arch }}{{ end }}
- {{- if .Arm }}v{{ .Arm }}{{ end }}
- files:
- - COPYRIGHT
- - LICENSE
- - README.md
- - Caddyfile
- - Caddyfile.dev
- format_overrides:
- - goos: windows
- format: zip
+ - builds:
+ - caddy
+ name_template: >-
+ {{ .ProjectName }}_
+ {{- title .Os }}_
+ {{- if eq .Arch "amd64" }}x86_64
+ {{- else if eq .Arch "386" }}i386
+ {{- else }}{{ .Arch }}{{ end }}
+ {{- if .Arm }}v{{ .Arm }}{{ end }}
+ files:
+ - COPYRIGHT
+ - LICENSE
+ - README.md
+ - Caddyfile
+ - Caddyfile.dev
+ format_overrides:
+ - goos: windows
+ format: zip
dockers:
-- ids:
- - caddy
- goos: linux
- goarch: amd64
- image_templates:
- - 'ghcr.io/devmachine-fr/mercure:{{ .Tag }}-amd64'
- - 'ghcr.io/devmachine-fr/mercure:v{{ .Major }}-amd64'
- - 'ghcr.io/devmachine-fr/mercure:v{{ .Major }}.{{ .Minor }}-amd64'
- - 'ghcr.io/devmachine-fr/mercure:latest-amd64'
- use: buildx
- build_flag_templates:
- - "--platform=linux/amd64"
- extra_files:
- - Caddyfile
- - Caddyfile.dev
-- ids:
- - caddy
- goos: linux
- goarch: arm64
- image_templates:
- - 'ghcr.io/devmachine-fr/mercure:{{ .Tag }}-arm64v8'
- - 'ghcr.io/devmachine-fr/mercure:v{{ .Major }}-arm64v8'
- - 'ghcr.io/devmachine-fr/mercure:v{{ .Major }}.{{ .Minor }}-arm64v8'
- - 'ghcr.io/devmachine-fr/mercure:latest-arm64v8'
- use: buildx
- build_flag_templates:
- - "--platform=linux/arm64/v8"
- extra_files:
- - Caddyfile
- - Caddyfile.dev
+ - ids:
+ - caddy
+ goos: linux
+ goarch: amd64
+ image_templates:
+ - "ghcr.io/devmachine-fr/mercure:{{ .Tag }}-amd64"
+ - "ghcr.io/devmachine-fr/mercure:v{{ .Major }}-amd64"
+ - "ghcr.io/devmachine-fr/mercure:v{{ .Major }}.{{ .Minor }}-amd64"
+ - "ghcr.io/devmachine-fr/mercure:latest-amd64"
+ use: buildx
+ build_flag_templates:
+ - "--platform=linux/amd64"
+ extra_files:
+ - Caddyfile
+ - Caddyfile.dev
+ - ids:
+ - caddy
+ goos: linux
+ goarch: arm64
+ image_templates:
+ - "ghcr.io/devmachine-fr/mercure:{{ .Tag }}-arm64v8"
+ - "ghcr.io/devmachine-fr/mercure:v{{ .Major }}-arm64v8"
+ - "ghcr.io/devmachine-fr/mercure:v{{ .Major }}.{{ .Minor }}-arm64v8"
+ - "ghcr.io/devmachine-fr/mercure:latest-arm64v8"
+ use: buildx
+ build_flag_templates:
+ - "--platform=linux/arm64/v8"
+ extra_files:
+ - Caddyfile
+ - Caddyfile.dev
docker_manifests:
-- name_template: ghcr.io/devmachine-fr/mercure:{{ .Tag }}
- image_templates:
- - ghcr.io/devmachine-fr/mercure:{{ .Tag }}-amd64
- - ghcr.io/devmachine-fr/mercure:{{ .Tag }}-arm64v8
-- name_template: ghcr.io/devmachine-fr/mercure:v{{ .Major }}
- image_templates:
- - ghcr.io/devmachine-fr/mercure:v{{ .Major }}-amd64
- - ghcr.io/devmachine-fr/mercure:v{{ .Major }}-arm64v8
-- name_template: ghcr.io/devmachine-fr/mercure:v{{ .Major }}.{{ .Minor }}
- image_templates:
- - ghcr.io/devmachine-fr/mercure:v{{ .Major }}.{{ .Minor }}-amd64
- - ghcr.io/devmachine-fr/mercure:v{{ .Major }}.{{ .Minor }}-arm64v8
-- name_template: ghcr.io/devmachine-fr/mercure:latest
- image_templates:
- - ghcr.io/devmachine-fr/mercure:latest-amd64
- - ghcr.io/devmachine-fr/mercure:latest-arm64v8
\ No newline at end of file
+ - name_template: ghcr.io/devmachine-fr/mercure:{{ .Tag }}
+ image_templates:
+ - ghcr.io/devmachine-fr/mercure:{{ .Tag }}-amd64
+ - ghcr.io/devmachine-fr/mercure:{{ .Tag }}-arm64v8
+ - name_template: ghcr.io/devmachine-fr/mercure:v{{ .Major }}
+ image_templates:
+ - ghcr.io/devmachine-fr/mercure:v{{ .Major }}-amd64
+ - ghcr.io/devmachine-fr/mercure:v{{ .Major }}-arm64v8
+ - name_template: ghcr.io/devmachine-fr/mercure:v{{ .Major }}.{{ .Minor }}
+ image_templates:
+ - ghcr.io/devmachine-fr/mercure:v{{ .Major }}.{{ .Minor }}-amd64
+ - ghcr.io/devmachine-fr/mercure:v{{ .Major }}.{{ .Minor }}-arm64v8
+ - name_template: ghcr.io/devmachine-fr/mercure:latest
+ image_templates:
+ - ghcr.io/devmachine-fr/mercure:latest-amd64
+ - ghcr.io/devmachine-fr/mercure:latest-arm64v8
+signs:
+ - artifacts: checksum
+ args:
+ [
+ "--batch",
+ "-u",
+ "{{ .Env.GPG_FINGERPRINT }}",
+ "--output",
+ "${signature}",
+ "--detach-sign",
+ "${artifact}",
+ ]
diff --git a/.markdownlintignore b/.markdownlintignore
new file mode 100644
index 00000000..c056d53a
--- /dev/null
+++ b/.markdownlintignore
@@ -0,0 +1 @@
+spec/**
diff --git a/.textlintignore b/.textlintignore
new file mode 100644
index 00000000..8cc0e7a6
--- /dev/null
+++ b/.textlintignore
@@ -0,0 +1 @@
+spec/mercure.txt
diff --git a/.vscode/launch.json b/.vscode/launch.json
new file mode 100644
index 00000000..6e262022
--- /dev/null
+++ b/.vscode/launch.json
@@ -0,0 +1,21 @@
+{
+ "version": "0.2.0",
+ "configurations": [
+ {
+ "name": "Launch the hub",
+ "type": "go",
+ "request": "launch",
+ "mode": "auto",
+ "program": "${workspaceFolder}/caddy/mercure",
+ "env": {
+ "MERCURE_PUBLISHER_JWT_KEY": "!ChangeThisMercureHubJWTSecretKey!",
+ "MERCURE_SUBSCRIBER_JWT_KEY": "!ChangeThisMercureHubJWTSecretKey!",
+ "MERCURE_EXTRA_DIRECTIVES": "anonymous\nwrite_timeout 10s",
+ "GLOBAL_OPTIONS": "debug",
+ "SERVER_NAME": "localhost, host.docker.internal",
+ "CADDY_SERVER_EXTRA_DIRECTIVES": "tls internal"
+ },
+ "args": ["run", "--config", "../../Caddyfile.dev"]
+ }
+ ]
+}
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 19b6d15b..24d3140a 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -14,7 +14,7 @@ If you include code from another project, please mention it in the Pull Request
The commit message must follow the [Conventional Commits specification](https://www.conventionalcommits.org/).
The following types are allowed:
-* `fix`: bug fix
+* `fix`: bugfix
* `feat`: new feature
* `docs`: change in the documentation
* `spec`: spec change
@@ -48,7 +48,7 @@ To run the test suite:
To test the Caddy module:
cd caddy/mercure
- MERCURE_PUBLISHER_JWT_KEY='!ChangeMe!' MERCURE_SUBSCRIBER_JWT_KEY='!ChangeMe!' go run main.go run -config ../../Caddyfile.dev
+ MERCURE_PUBLISHER_JWT_KEY='!ChangeThisMercureHubJWTSecretKey!' MERCURE_SUBSCRIBER_JWT_KEY='!ChangeThisMercureHubJWTSecretKey!' go run main.go run --config ../../Caddyfile.dev
Go to `https://localhost` and enjoy!
@@ -67,29 +67,10 @@ When you send a PR, make sure that:
* You make the PR on the same branch you based your changes on. If you see commits
that you did not make in your PR, you're doing it wrong.
-### Configuring VSCode
-
-`.vscode/launch.json`:
-
-```json
-{
- "version": "0.2.0",
- "configurations": [
- {
- "name": "Launch the hub",
- "type": "go",
- "request": "launch",
- "mode": "auto",
- "program": "${workspaceFolder}",
- "env": {
- "MERCURE_PUBLISHER_JWT_KEY": "!ChangeMe!",
- "MERCURE_SUBSCRIBER_JWT_KEY": "!ChangeMe!"
- },
- "args": ["run", "-config", "Caddyfile.dev"]
- }
- ]
-}
-```
+### Configuring Visual Studio Code
+
+A configuration for VSCode is provided in the `.vscode/` directory of the repository.
+It is automatically loaded by VS Code.
### Finding Deadlocks
@@ -110,7 +91,6 @@ To contribute to the protocol itself:
* Make your changes
* [Download Mmark](https://github.com/mmarkdown/mmark/releases)
* [Download `xml2rfc` using pip](https://pypi.org/project/xml2rfc/): `pip install xml2rfc`
-* Format the Markdown file: `mmark -markdown -w spec/mercure.md`
* Generate the XML file: `mmark spec/mercure.md > spec/mercure.xml`
* Validate the generated XML file and generate the text file: `xml2rfc --text --v3 spec/mercure.xml`
* Remove non-ASCII characters from the generated `mercure.txt` file (example: K**é**vin)
diff --git a/Caddyfile b/Caddyfile
index c0b555ba..f6ddfb4a 100644
--- a/Caddyfile
+++ b/Caddyfile
@@ -1,13 +1,25 @@
# Learn how to configure the Mercure.rocks Hub on https://mercure.rocks/docs/hub/config
{
+ order mercure after encode
+
{$GLOBAL_OPTIONS}
}
-{$SERVER_NAME:localhost}
+{$CADDY_EXTRA_CONFIG}
-log
+{$SERVER_NAME:localhost} {
+ log {
+ format filter {
+ # Defaults to json while waiting for https://github.com/caddyserver/caddy/pull/5980
+ wrap json
+ fields {
+ uri query {
+ replace authorization REDACTED
+ }
+ }
+ }
+ }
-route {
encode zstd gzip
mercure {
@@ -21,7 +33,23 @@ route {
{$MERCURE_EXTRA_DIRECTIVES}
}
- respond /healthz 200
+ {$CADDY_SERVER_EXTRA_DIRECTIVES}
+ skip_log /robots.txt
+ skip_log /healthz
+ skip_log /favicon.ico
+
+ header / Content-Type "text/html; charset=utf-8"
+ respond / `
+
+
+
+
Welcome to Mercure
+Welcome to Mercure
+The URL of your hub is /.well-known/mercure
.
+Read the documentation on Mercure.rocks, real-time apps made easy .`
+ respond /robots.txt `User-agent: *
+Disallow: /`
+ respond /healthz 200
respond "Not Found" 404
}
diff --git a/Caddyfile.dev b/Caddyfile.dev
index 98ecb150..a1bf39b2 100644
--- a/Caddyfile.dev
+++ b/Caddyfile.dev
@@ -1,14 +1,25 @@
# Learn how to configure the Mercure.rocks Hub on https://mercure.rocks/docs/hub/config
{
+ order mercure after encode
+
{$GLOBAL_OPTIONS}
}
-{$SERVER_NAME:localhost}
+{$CADDY_EXTRA_CONFIG}
-log
+{$SERVER_NAME:localhost} {
+ log {
+ format filter {
+ # Defaults to console while waiting for https://github.com/caddyserver/caddy/pull/5980
+ wrap console
+ fields {
+ uri query {
+ replace authorization REDACTED
+ }
+ }
+ }
+ }
-route {
- redir / /.well-known/mercure/ui/
encode zstd gzip
mercure {
@@ -28,7 +39,12 @@ route {
{$MERCURE_EXTRA_DIRECTIVES}
}
- respond /healthz 200
+ {$CADDY_SERVER_EXTRA_DIRECTIVES}
+ redir / /.well-known/mercure/ui/
+
+ respond /healthz 200
+ respond /robots.txt `User-agent: *
+Disallow: /`
respond "Not Found" 404
}
diff --git a/Dockerfile.legacy b/Dockerfile.legacy
index e801d543..1911bb6a 100644
--- a/Dockerfile.legacy
+++ b/Dockerfile.legacy
@@ -1,3 +1,4 @@
+# hadolint ignore=DL3006
FROM gcr.io/distroless/static
COPY mercure /
CMD ["/mercure"]
diff --git a/README.md b/README.md
index 65806f33..d2e742cd 100644
--- a/README.md
+++ b/README.md
@@ -2,13 +2,13 @@
*Protocol and Reference Implementation*
-Mercure is a protocol allowing to push data updates to web browsers and other HTTP clients in a convenient, fast, reliable and battery-efficient way.
+Mercure is a protocol for pushing data updates to web browsers and other HTTP clients in a convenient, fast, reliable, and battery-efficient way.
It is especially useful to publish async and real-time updates of resources served through web APIs, to reactive web and mobile apps.
[![Awesome](https://awesome.re/badge.svg)](docs/ecosystem/awesome.md)
[![Artifact HUB](https://img.shields.io/endpoint?url=https://artifacthub.io/badge/repository/mercure)](https://artifacthub.io/packages/search?repo=mercure)
[![PkgGoDev](https://pkg.go.dev/badge/github.com/dunglas/mercure)](https://pkg.go.dev/github.com/dunglas/mercure)
-![CI](https://github.com/dunglas/mercure/workflows/CI/badge.svg)
+[![CI](https://github.com/dunglas/mercure/actions/workflows/ci.yml/badge.svg)](https://github.com/dunglas/mercure/actions/workflows/ci.yml)
[![Coverage Status](https://coveralls.io/repos/github/dunglas/mercure/badge.svg?branch=master)](https://coveralls.io/github/dunglas/mercure?branch=master)
[![Go Report Card](https://goreportcard.com/badge/github.com/dunglas/mercure)](https://goreportcard.com/report/github.com/dunglas/mercure)
@@ -18,10 +18,10 @@ It is especially useful to publish async and real-time updates of resources serv
* [Full documentation](https://mercure.rocks/docs)
* [Demo](https://demo.mercure.rocks/)
-The protocol has been published as [an Internet Draft](https://datatracker.ietf.org/doc/draft-dunglas-mercure/) that [is maintained in this repository](https://mercure.rocks/spec).
+[The protocol](https://mercure.rocks/spec) is maintained in this repository and is also available as [an Internet-Draft](https://datatracker.ietf.org/doc/draft-dunglas-mercure/).
A reference, production-grade, implementation of [**a Mercure hub**](https://mercure.rocks/docs/hub/install) (the server) is also available in this repository.
-It's a free software (AGPL) written in Go. It is provided along with a library that can be used in any Go application to implement the Mercure protocol directly (without a hub) and an official Docker image.
+It's free software (AGPL) written in Go. It is provided along with a library that can be used in any Go application to implement the Mercure protocol directly (without a hub) and [an official Docker image](https://hub.docker.com/r/dunglas/mercure).
In addition, a managed and high-scalability version of the Mercure.rocks hub is [available on Mercure.rocks](https://mercure.rocks/pricing).
@@ -31,7 +31,7 @@ See [CONTRIBUTING.md](CONTRIBUTING.md).
## License and Copyright
-See https://mercure.rocks/docs/hub/license.
+See [license information](https://mercure.rocks/docs/hub/license).
## Credits
diff --git a/authorization.go b/authorization.go
index 4efbca9d..c073b77e 100644
--- a/authorization.go
+++ b/authorization.go
@@ -6,7 +6,8 @@ import (
"net/http"
"net/url"
- "github.com/golang-jwt/jwt/v4"
+ "github.com/golang-jwt/jwt/v5"
+ "go.uber.org/zap"
)
// claims contains Mercure's JWT claims.
@@ -27,6 +28,7 @@ type role int
const (
defaultCookieName = "mercureAuthorization"
+ bearerPrefix = "Bearer "
roleSubscriber role = iota
rolePublisher
)
@@ -34,12 +36,12 @@ const (
var (
// ErrInvalidAuthorizationHeader is returned when the Authorization header is invalid.
ErrInvalidAuthorizationHeader = errors.New(`invalid "Authorization" HTTP header`)
+ // ErrInvalidAuthorizationQuery is returned when the authorization query parameter is invalid.
+ ErrInvalidAuthorizationQuery = errors.New(`invalid "authorization" Query parameter`)
// ErrNoOrigin is returned when the cookie authorization mechanism is used and no Origin nor Referer headers are presents.
ErrNoOrigin = errors.New(`an "Origin" or a "Referer" HTTP header must be present to use the cookie-based authorization mechanism`)
// ErrOriginNotAllowed is returned when the Origin is not allowed to post updates.
ErrOriginNotAllowed = errors.New("origin not allowed to post updates")
- // ErrUnexpectedSigningMethod is returned when the signing JWT method is not supported.
- ErrUnexpectedSigningMethod = errors.New("unexpected signing method")
// ErrInvalidJWT is returned when the JWT is invalid.
ErrInvalidJWT = errors.New("invalid JWT")
// ErrPublicKey is returned when there is an error with the public key.
@@ -48,14 +50,22 @@ var (
// Authorize validates the JWT that may be provided through an "Authorization" HTTP header or an authorization cookie.
// It returns the claims contained in the token if it exists and is valid, nil if no token is provided (anonymous mode), and an error if the token is not valid.
-func authorize(r *http.Request, jwtConfig *jwtConfig, publishOrigins []string, cookieName string) (*claims, error) {
+func authorize(r *http.Request, jwtKeyfunc jwt.Keyfunc, publishOrigins []string, cookieName string) (*claims, error) {
authorizationHeaders, headerExists := r.Header["Authorization"]
if headerExists {
- if len(authorizationHeaders) != 1 || len(authorizationHeaders[0]) < 48 || authorizationHeaders[0][:7] != "Bearer " {
+ if len(authorizationHeaders) != 1 || len(authorizationHeaders[0]) < 48 || authorizationHeaders[0][:7] != bearerPrefix {
return nil, ErrInvalidAuthorizationHeader
}
- return validateJWT(authorizationHeaders[0][7:], jwtConfig)
+ return validateJWT(authorizationHeaders[0][7:], jwtKeyfunc)
+ }
+
+ if authorizationQuery, queryExists := r.URL.Query()["authorization"]; queryExists {
+ if len(authorizationQuery) != 1 || len(authorizationQuery[0]) < 41 {
+ return nil, ErrInvalidAuthorizationQuery
+ }
+
+ return validateJWT(authorizationQuery[0], jwtKeyfunc)
}
cookie, err := r.Cookie(cookieName)
@@ -65,8 +75,8 @@ func authorize(r *http.Request, jwtConfig *jwtConfig, publishOrigins []string, c
}
// CSRF attacks cannot occur when using safe methods
- if r.Method != "POST" {
- return validateJWT(cookie.Value, jwtConfig)
+ if r.Method != http.MethodPost {
+ return validateJWT(cookie.Value, jwtKeyfunc)
}
origin := r.Header.Get("Origin")
@@ -87,7 +97,7 @@ func authorize(r *http.Request, jwtConfig *jwtConfig, publishOrigins []string, c
for _, allowedOrigin := range publishOrigins {
if allowedOrigin == "*" || origin == allowedOrigin {
- return validateJWT(cookie.Value, jwtConfig)
+ return validateJWT(cookie.Value, jwtKeyfunc)
}
}
@@ -95,22 +105,8 @@ func authorize(r *http.Request, jwtConfig *jwtConfig, publishOrigins []string, c
}
// validateJWT validates that the provided JWT token is a valid Mercure token.
-func validateJWT(encodedToken string, jwtConfig *jwtConfig) (*claims, error) {
- token, err := jwt.ParseWithClaims(encodedToken, &claims{}, func(token *jwt.Token) (interface{}, error) {
- switch jwtConfig.signingMethod.(type) {
- case *jwt.SigningMethodHMAC:
- return jwtConfig.key, nil
- case *jwt.SigningMethodRSA:
- pub, err := jwt.ParseRSAPublicKeyFromPEM(jwtConfig.key)
- if err != nil {
- return nil, fmt.Errorf("unable to parse RSA public key: %w", err)
- }
-
- return pub, nil
- }
-
- return nil, fmt.Errorf("%T: %w", jwtConfig.signingMethod, ErrUnexpectedSigningMethod)
- })
+func validateJWT(encodedToken string, jwtKeyfunc jwt.Keyfunc) (*claims, error) {
+ token, err := jwt.ParseWithClaims(encodedToken, &claims{}, jwtKeyfunc)
if err != nil {
return nil, fmt.Errorf("unable to parse JWT: %w", err)
}
@@ -160,3 +156,10 @@ func canDispatch(s *TopicSelectorStore, topics, topicSelectors []string) bool {
return true
}
+
+func (h *Hub) httpAuthorizationError(w http.ResponseWriter, r *http.Request, err error) {
+ http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
+ if c := h.logger.Check(zap.DebugLevel, "Topic selectors not matched, not provided or authorization error"); c != nil {
+ c.Write(zap.String("remote_addr", r.RemoteAddr), zap.Error(err))
+ }
+}
diff --git a/authorization_test.go b/authorization_test.go
index 5e6881bb..a4eb82d6 100644
--- a/authorization_test.go
+++ b/authorization_test.go
@@ -4,7 +4,7 @@ import (
"net/http"
"testing"
- "github.com/golang-jwt/jwt/v4"
+ "github.com/golang-jwt/jwt/v5"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -29,6 +29,7 @@ bCd7nPuNAyYHCOOHAgMBAAE=
-----END PUBLIC KEY-----
`
+//nolint:gosec
const privateKeyRsa = `-----BEGIN RSA PRIVATE KEY-----
MIICWwIBAAKBgHVwuJsFmzsFnOkGj+OgAp4lTNqRCF0RZSmjY+ECWOJ3sSEzQ8qt
kJe61uSjr/PKmqvBxxex0YtUL7waSS4jvq3ws8BmWIxK2GqoAVjLjK8HzThSPQpg
@@ -64,344 +65,470 @@ Utb8p35tfj97usdiEB0AN8ray4wZbVWj
-----END CERTIFICATE-----
`
+var keyfuncHS256 = func() jwt.Keyfunc {
+ keyfunc, _ := createJWTKeyfunc([]byte("!ChangeMe!"), "HS256")
+
+ return keyfunc
+}()
+
+var keyfuncRS256publicKey = func() jwt.Keyfunc {
+ keyfunc, _ := createJWTKeyfunc([]byte(publicKeyRsa), "RS256")
+
+ return keyfunc
+}()
+
func TestAuthorizeMultipleAuthorizationHeader(t *testing.T) {
- r, _ := http.NewRequest("GET", defaultHubURL, nil)
+ r, _ := http.NewRequest(http.MethodGet, defaultHubURL, nil)
r.Header.Add("Authorization", validEmptyHeader)
r.Header.Add("Authorization", validEmptyHeader)
- claims, err := authorize(r, &jwtConfig{[]byte{}, jwt.SigningMethodHS256}, []string{}, defaultCookieName)
- assert.EqualError(t, err, "invalid \"Authorization\" HTTP header")
- assert.Nil(t, claims)
+ keyfunc, _ := createJWTKeyfunc([]byte{}, "HS256")
+
+ claims, err := authorize(r, keyfunc, []string{}, defaultCookieName)
+ require.Error(t, err, `invalid "Authorization" HTTP header`)
+ require.Nil(t, claims)
}
func TestAuthorizeMultipleAuthorizationHeaderRsa(t *testing.T) {
- r, _ := http.NewRequest("GET", defaultHubURL, nil)
+ r, _ := http.NewRequest(http.MethodGet, defaultHubURL, nil)
r.Header.Add("Authorization", validEmptyHeaderRsa)
r.Header.Add("Authorization", validEmptyHeaderRsa)
- claims, err := authorize(r, &jwtConfig{[]byte{}, jwt.SigningMethodRS256}, []string{}, defaultCookieName)
- assert.EqualError(t, err, "invalid \"Authorization\" HTTP header")
- assert.Nil(t, claims)
+ keyfunc, _ := createJWTKeyfunc([]byte{}, "RS256")
+
+ claims, err := authorize(r, keyfunc, []string{}, defaultCookieName)
+ require.Error(t, err, `invalid "Authorization" HTTP header`)
+ require.Nil(t, claims)
}
func TestAuthorizeAuthorizationHeaderTooShort(t *testing.T) {
- r, _ := http.NewRequest("GET", defaultHubURL, nil)
+ r, _ := http.NewRequest(http.MethodGet, defaultHubURL, nil)
r.Header.Add("Authorization", "Bearer x")
- claims, err := authorize(r, &jwtConfig{[]byte{}, jwt.SigningMethodHS256}, []string{}, defaultCookieName)
- assert.EqualError(t, err, "invalid \"Authorization\" HTTP header")
- assert.Nil(t, claims)
+ keyfunc, _ := createJWTKeyfunc([]byte{}, "HS256")
+
+ claims, err := authorize(r, keyfunc, []string{}, defaultCookieName)
+ require.Error(t, err, `invalid "Authorization" HTTP header`)
+ require.Nil(t, claims)
}
func TestAuthorizeAuthorizationHeaderNoBearer(t *testing.T) {
- r, _ := http.NewRequest("GET", defaultHubURL, nil)
+ r, _ := http.NewRequest(http.MethodGet, defaultHubURL, nil)
r.Header.Add("Authorization", "Greater "+validEmptyHeader)
- claims, err := authorize(r, &jwtConfig{[]byte{}, jwt.SigningMethodHS256}, []string{}, defaultCookieName)
- assert.EqualError(t, err, "invalid \"Authorization\" HTTP header")
- assert.Nil(t, claims)
+ keyfunc, _ := createJWTKeyfunc([]byte{}, "HS256")
+
+ claims, err := authorize(r, keyfunc, []string{}, defaultCookieName)
+ require.EqualError(t, err, `invalid "Authorization" HTTP header`)
+ require.Nil(t, claims)
}
func TestAuthorizeAuthorizationHeaderNoBearerRsa(t *testing.T) {
- r, _ := http.NewRequest("GET", defaultHubURL, nil)
+ r, _ := http.NewRequest(http.MethodGet, defaultHubURL, nil)
r.Header.Add("Authorization", "Greater "+validEmptyHeaderRsa)
- claims, err := authorize(r, &jwtConfig{[]byte{}, jwt.SigningMethodRS256}, []string{}, defaultCookieName)
- assert.EqualError(t, err, "invalid \"Authorization\" HTTP header")
- assert.Nil(t, claims)
+ keyfunc, _ := createJWTKeyfunc([]byte{}, "RS256")
+
+ claims, err := authorize(r, keyfunc, []string{}, defaultCookieName)
+ require.EqualError(t, err, `invalid "Authorization" HTTP header`)
+ require.Nil(t, claims)
}
func TestAuthorizeAuthorizationHeaderInvalidAlg(t *testing.T) {
- r, _ := http.NewRequest("GET", defaultHubURL, nil)
- r.Header.Add("Authorization", "Bearer "+createDummyNoneSignedJWT())
+ r, _ := http.NewRequest(http.MethodGet, defaultHubURL, nil)
+ r.Header.Add("Authorization", bearerPrefix+createDummyNoneSignedJWT())
- claims, err := authorize(r, &jwtConfig{[]byte{}, jwt.SigningMethodHS256}, []string{}, defaultCookieName)
- assert.EqualError(t, err, "unable to parse JWT: 'none' signature type is not allowed")
- assert.Nil(t, claims)
+ keyfunc, _ := createJWTKeyfunc([]byte{}, "HS256")
+
+ claims, err := authorize(r, keyfunc, []string{}, defaultCookieName)
+ require.EqualError(t, err, "unable to parse JWT: token is unverifiable: error while executing keyfunc: *jwt.signingMethodNone: unexpected signing method")
+ require.Nil(t, claims)
}
func TestAuthorizeAuthorizationHeaderInvalidKey(t *testing.T) {
- r, _ := http.NewRequest("GET", defaultHubURL, nil)
- r.Header.Add("Authorization", "Bearer "+validEmptyHeader)
+ r, _ := http.NewRequest(http.MethodGet, defaultHubURL, nil)
+ r.Header.Add("Authorization", bearerPrefix+validEmptyHeader)
- claims, err := authorize(r, &jwtConfig{[]byte{}, jwt.SigningMethodHS256}, []string{}, defaultCookieName)
- assert.EqualError(t, err, "unable to parse JWT: signature is invalid")
- assert.Nil(t, claims)
-}
+ keyfunc, _ := createJWTKeyfunc([]byte{}, "HS256")
-func TestAuthorizeAuthorizationHeaderInvalidKeyRsa(t *testing.T) {
- r, _ := http.NewRequest("GET", defaultHubURL, nil)
- r.Header.Add("Authorization", "Bearer "+validEmptyHeaderRsa)
-
- claims, err := authorize(r, &jwtConfig{[]byte{}, jwt.SigningMethodRS256}, []string{}, defaultCookieName)
- assert.EqualError(t, err, "unable to parse JWT: unable to parse RSA public key: invalid key: Key must be a PEM encoded PKCS1 or PKCS8 key")
- assert.Nil(t, claims)
+ claims, err := authorize(r, keyfunc, []string{}, defaultCookieName)
+ require.EqualError(t, err, "unable to parse JWT: token signature is invalid: signature is invalid")
+ require.Nil(t, claims)
}
func TestAuthorizeAuthorizationHeaderNoContent(t *testing.T) {
- r, _ := http.NewRequest("GET", defaultHubURL, nil)
- r.Header.Add("Authorization", "Bearer "+validEmptyHeader)
+ r, _ := http.NewRequest(http.MethodGet, defaultHubURL, nil)
+ r.Header.Add("Authorization", bearerPrefix+validEmptyHeader)
- claims, err := authorize(r, &jwtConfig{[]byte("!ChangeMe!"), jwt.SigningMethodHS256}, []string{}, defaultCookieName)
- require.Nil(t, err)
- assert.Nil(t, claims.Mercure.Publish)
- assert.Nil(t, claims.Mercure.Subscribe)
+ claims, err := authorize(r, keyfuncHS256, []string{}, defaultCookieName)
+ require.NoError(t, err)
+ require.Nil(t, claims.Mercure.Publish)
+ require.Nil(t, claims.Mercure.Subscribe)
}
func TestAuthorizeAuthorizationHeaderNoContentRsa(t *testing.T) {
- r, _ := http.NewRequest("GET", defaultHubURL, nil)
- r.Header.Add("Authorization", "Bearer "+validEmptyHeaderRsa)
+ r, _ := http.NewRequest(http.MethodGet, defaultHubURL, nil)
+ r.Header.Add("Authorization", bearerPrefix+validEmptyHeaderRsa)
- claims, err := authorize(r, &jwtConfig{[]byte(publicKeyRsa), jwt.SigningMethodRS256}, []string{}, defaultCookieName)
- require.Nil(t, err)
- assert.Nil(t, claims.Mercure.Publish)
- assert.Nil(t, claims.Mercure.Subscribe)
+ claims, err := authorize(r, keyfuncRS256publicKey, []string{}, defaultCookieName)
+ require.NoError(t, err)
+ require.Nil(t, claims.Mercure.Publish)
+ require.Nil(t, claims.Mercure.Subscribe)
}
func TestAuthorizeAuthorizationHeader(t *testing.T) {
- r, _ := http.NewRequest("GET", defaultHubURL, nil)
- r.Header.Add("Authorization", "Bearer "+validFullHeader)
+ r, _ := http.NewRequest(http.MethodGet, defaultHubURL, nil)
+ r.Header.Add("Authorization", bearerPrefix+validFullHeader)
- claims, err := authorize(r, &jwtConfig{[]byte("!ChangeMe!"), jwt.SigningMethodHS256}, []string{}, defaultCookieName)
- require.Nil(t, err)
+ claims, err := authorize(r, keyfuncHS256, []string{}, defaultCookieName)
+ require.NoError(t, err)
assert.Equal(t, []string{"foo", "bar"}, claims.Mercure.Publish)
assert.Equal(t, []string{"foo", "baz"}, claims.Mercure.Subscribe)
}
func TestAuthorizeAuthorizationHeaderRsa(t *testing.T) {
- r, _ := http.NewRequest("GET", defaultHubURL, nil)
- r.Header.Add("Authorization", "Bearer "+validFullHeaderRsa)
+ r, _ := http.NewRequest(http.MethodGet, defaultHubURL, nil)
+ r.Header.Add("Authorization", bearerPrefix+validFullHeaderRsa)
- claims, err := authorize(r, &jwtConfig{[]byte(publicKeyRsa), jwt.SigningMethodRS256}, []string{}, defaultCookieName)
- require.Nil(t, err)
+ claims, err := authorize(r, keyfuncRS256publicKey, []string{}, defaultCookieName)
+ require.NoError(t, err)
assert.Equal(t, []string{"foo", "bar"}, claims.Mercure.Publish)
assert.Equal(t, []string{"foo", "baz"}, claims.Mercure.Subscribe)
}
func TestAuthorizeAuthorizationHeaderNamespacedRsa(t *testing.T) {
- r, _ := http.NewRequest("GET", defaultHubURL, nil)
- r.Header.Add("Authorization", "Bearer "+validFullHeaderNamespacedRsa)
+ r, _ := http.NewRequest(http.MethodGet, defaultHubURL, nil)
+ r.Header.Add("Authorization", bearerPrefix+validFullHeaderNamespacedRsa)
- claims, err := authorize(r, &jwtConfig{[]byte(publicKeyRsa), jwt.SigningMethodRS256}, []string{}, defaultCookieName)
- require.Nil(t, err)
+ claims, err := authorize(r, keyfuncRS256publicKey, []string{}, defaultCookieName)
+ require.NoError(t, err)
assert.Equal(t, []string{"foo", "bar"}, claims.Mercure.Publish)
assert.Equal(t, []string{"foo", "baz"}, claims.Mercure.Subscribe)
}
func TestAuthorizeAuthorizationHeaderRsaWithCert(t *testing.T) {
- r, _ := http.NewRequest("GET", defaultHubURL, nil)
- r.Header.Add("Authorization", "Bearer "+validFullHeaderRsaForCert)
+ r, _ := http.NewRequest(http.MethodGet, defaultHubURL, nil)
+ r.Header.Add("Authorization", bearerPrefix+validFullHeaderRsaForCert)
- claims, err := authorize(r, &jwtConfig{[]byte(certificateRsa), jwt.SigningMethodRS256}, []string{}, defaultCookieName)
- require.Nil(t, err)
+ keyfunc, _ := createJWTKeyfunc([]byte(certificateRsa), "RS256")
+
+ claims, err := authorize(r, keyfunc, []string{}, defaultCookieName)
+ require.NoError(t, err)
assert.Equal(t, []string{"foo", "bar"}, claims.Mercure.Publish)
assert.Equal(t, []string{"foo", "baz"}, claims.Mercure.Subscribe)
}
func TestAuthorizeAuthorizationHeaderWrongAlgorithm(t *testing.T) {
- r, _ := http.NewRequest("GET", defaultHubURL, nil)
- r.Header.Add("Authorization", "Bearer "+validFullHeaderRsa)
+ r, _ := http.NewRequest(http.MethodGet, defaultHubURL, nil)
+ r.Header.Add("Authorization", bearerPrefix+validFullHeaderRsa)
- claims, err := authorize(r, &jwtConfig{[]byte(publicKeyRsa), nil}, []string{}, defaultCookieName)
- assert.EqualError(t, err, "unable to parse JWT: : unexpected signing method")
+ claims, err := authorize(r, keyfuncHS256, []string{}, defaultCookieName)
+ require.EqualError(t, err, "unable to parse JWT: token is unverifiable: error while executing keyfunc: *jwt.SigningMethodRSA: unexpected signing method")
assert.Nil(t, claims)
}
-func TestAuthorizeCookieInvalidAlg(t *testing.T) {
- r, _ := http.NewRequest("GET", defaultHubURL, nil)
- r.AddCookie(&http.Cookie{Name: defaultCookieName, Value: createDummyNoneSignedJWT()})
+func TestAuthorizeAuthorizationQueryTooShort(t *testing.T) {
+ r, _ := http.NewRequest(http.MethodGet, defaultHubURL, nil)
+ query := r.URL.Query()
+ query.Set("authorization", "x")
+ r.URL.RawQuery = query.Encode()
- claims, err := authorize(r, &jwtConfig{[]byte{}, jwt.SigningMethodHS256}, []string{}, defaultCookieName)
- assert.EqualError(t, err, "unable to parse JWT: 'none' signature type is not allowed")
- assert.Nil(t, claims)
+ keyfunc, _ := createJWTKeyfunc([]byte{}, "HS256")
+
+ claims, err := authorize(r, keyfunc, []string{}, defaultCookieName)
+ require.EqualError(t, err, `invalid "authorization" Query parameter`)
+ require.Nil(t, claims)
}
-func TestAuthorizeCookieInvalidKey(t *testing.T) {
- r, _ := http.NewRequest("GET", defaultHubURL, nil)
- r.AddCookie(&http.Cookie{Name: defaultCookieName, Value: validEmptyHeader})
+func TestAuthorizeAuthorizationQueryInvalidAlg(t *testing.T) {
+ r, _ := http.NewRequest(http.MethodGet, defaultHubURL, nil)
+ query := r.URL.Query()
+ query.Set("authorization", createDummyNoneSignedJWT())
+ r.URL.RawQuery = query.Encode()
- claims, err := authorize(r, &jwtConfig{[]byte{}, jwt.SigningMethodHS256}, []string{}, defaultCookieName)
- assert.EqualError(t, err, "unable to parse JWT: signature is invalid")
- assert.Nil(t, claims)
+ keyfunc, _ := createJWTKeyfunc([]byte{}, "HS256")
+
+ claims, err := authorize(r, keyfunc, []string{}, defaultCookieName)
+ require.EqualError(t, err, "unable to parse JWT: token is unverifiable: error while executing keyfunc: *jwt.signingMethodNone: unexpected signing method")
+ require.Nil(t, claims)
}
-func TestAuthorizeCookieEmptyKeyRsa(t *testing.T) {
- r, _ := http.NewRequest("GET", defaultHubURL, nil)
- r.AddCookie(&http.Cookie{Name: defaultCookieName, Value: validEmptyHeaderRsa})
+func TestAuthorizeAuthorizationQueryInvalidKey(t *testing.T) {
+ r, _ := http.NewRequest(http.MethodGet, defaultHubURL, nil)
+ query := r.URL.Query()
+ query.Set("authorization", validEmptyHeader)
+ r.URL.RawQuery = query.Encode()
- claims, err := authorize(r, &jwtConfig{[]byte{}, jwt.SigningMethodRS256}, []string{}, defaultCookieName)
- assert.EqualError(t, err, "unable to parse JWT: unable to parse RSA public key: invalid key: Key must be a PEM encoded PKCS1 or PKCS8 key")
- assert.Nil(t, claims)
+ keyfunc, _ := createJWTKeyfunc([]byte{}, "HS256")
+
+ claims, err := authorize(r, keyfunc, []string{}, defaultCookieName)
+ require.EqualError(t, err, "unable to parse JWT: token signature is invalid: signature is invalid")
+ require.Nil(t, claims)
}
-func TestAuthorizeCookieInvalidKeyRsa(t *testing.T) {
- r, _ := http.NewRequest("GET", defaultHubURL, nil)
- r.AddCookie(&http.Cookie{Name: defaultCookieName, Value: validEmptyHeaderRsa})
+func TestAuthorizeAuthorizationQueryNoContent(t *testing.T) {
+ r, _ := http.NewRequest(http.MethodGet, defaultHubURL, nil)
+ query := r.URL.Query()
+ query.Set("authorization", validEmptyHeader)
+ r.URL.RawQuery = query.Encode()
- claims, err := authorize(r, &jwtConfig{[]byte(privateKeyRsa), jwt.SigningMethodRS256}, []string{}, defaultCookieName)
- assert.Error(t, err)
- assert.Contains(t, err.Error(), "unable to parse JWT: unable to parse RSA public key") // The error message changed in Go 1.17
- assert.Nil(t, claims)
+ claims, err := authorize(r, keyfuncHS256, []string{}, defaultCookieName)
+ require.NoError(t, err)
+ require.Nil(t, claims.Mercure.Publish)
+ require.Nil(t, claims.Mercure.Subscribe)
+}
+
+func TestAuthorizeAuthorizationQueryNoContentRsa(t *testing.T) {
+ r, _ := http.NewRequest(http.MethodGet, defaultHubURL, nil)
+ query := r.URL.Query()
+ query.Set("authorization", validEmptyHeaderRsa)
+ r.URL.RawQuery = query.Encode()
+
+ claims, err := authorize(r, keyfuncRS256publicKey, []string{}, defaultCookieName)
+ require.NoError(t, err)
+ require.Nil(t, claims.Mercure.Publish)
+ require.Nil(t, claims.Mercure.Subscribe)
+}
+
+func TestAuthorizeAuthorizationQuery(t *testing.T) {
+ r, _ := http.NewRequest(http.MethodGet, defaultHubURL, nil)
+ query := r.URL.Query()
+ query.Set("authorization", validFullHeader)
+ r.URL.RawQuery = query.Encode()
+
+ claims, err := authorize(r, keyfuncHS256, []string{}, defaultCookieName)
+ require.NoError(t, err)
+ assert.Equal(t, []string{"foo", "bar"}, claims.Mercure.Publish)
+ assert.Equal(t, []string{"foo", "baz"}, claims.Mercure.Subscribe)
+}
+
+func TestAuthorizeAuthorizationQueryRsa(t *testing.T) {
+ r, _ := http.NewRequest(http.MethodGet, defaultHubURL, nil)
+ query := r.URL.Query()
+ query.Set("authorization", validFullHeaderRsa)
+ r.URL.RawQuery = query.Encode()
+
+ claims, err := authorize(r, keyfuncRS256publicKey, []string{}, defaultCookieName)
+ require.NoError(t, err)
+ assert.Equal(t, []string{"foo", "bar"}, claims.Mercure.Publish)
+ assert.Equal(t, []string{"foo", "baz"}, claims.Mercure.Subscribe)
+}
+
+func TestAuthorizeAuthorizationQueryNamespacedRsa(t *testing.T) {
+ r, _ := http.NewRequest(http.MethodGet, defaultHubURL, nil)
+ query := r.URL.Query()
+ query.Set("authorization", validFullHeaderNamespacedRsa)
+ r.URL.RawQuery = query.Encode()
+
+ claims, err := authorize(r, keyfuncRS256publicKey, []string{}, defaultCookieName)
+ require.NoError(t, err)
+ assert.Equal(t, []string{"foo", "bar"}, claims.Mercure.Publish)
+ assert.Equal(t, []string{"foo", "baz"}, claims.Mercure.Subscribe)
+}
+
+func TestAuthorizeAuthorizationQueryRsaWithCert(t *testing.T) {
+ r, _ := http.NewRequest(http.MethodGet, defaultHubURL, nil)
+ query := r.URL.Query()
+ query.Set("authorization", validFullHeaderRsaForCert)
+ r.URL.RawQuery = query.Encode()
+
+ keyfunc, _ := createJWTKeyfunc([]byte(certificateRsa), "RS256")
+
+ claims, err := authorize(r, keyfunc, []string{}, defaultCookieName)
+ require.NoError(t, err)
+ assert.Equal(t, []string{"foo", "bar"}, claims.Mercure.Publish)
+ assert.Equal(t, []string{"foo", "baz"}, claims.Mercure.Subscribe)
+}
+
+func TestAuthorizeAuthorizationQueryWrongAlgorithm(t *testing.T) {
+ r, _ := http.NewRequest(http.MethodGet, defaultHubURL, nil)
+ query := r.URL.Query()
+ query.Set("authorization", validFullHeaderRsa)
+ r.URL.RawQuery = query.Encode()
+
+ claims, err := authorize(r, keyfuncHS256, []string{}, defaultCookieName)
+ require.EqualError(t, err, "unable to parse JWT: token is unverifiable: error while executing keyfunc: *jwt.SigningMethodRSA: unexpected signing method")
+ require.Nil(t, claims)
+}
+
+func TestAuthorizeCookieInvalidAlg(t *testing.T) {
+ r, _ := http.NewRequest(http.MethodGet, defaultHubURL, nil)
+ r.AddCookie(&http.Cookie{Name: defaultCookieName, Value: createDummyNoneSignedJWT()})
+
+ claims, err := authorize(r, keyfuncHS256, []string{}, defaultCookieName)
+ require.EqualError(t, err, "unable to parse JWT: token is unverifiable: error while executing keyfunc: *jwt.signingMethodNone: unexpected signing method")
+ require.Nil(t, claims)
+}
+
+func TestAuthorizeCookieInvalidKey(t *testing.T) {
+ r, _ := http.NewRequest(http.MethodGet, defaultHubURL, nil)
+ r.AddCookie(&http.Cookie{Name: defaultCookieName, Value: validEmptyHeader})
+
+ keyfunc, _ := createJWTKeyfunc([]byte{}, "HS256")
+
+ claims, err := authorize(r, keyfunc, []string{}, defaultCookieName)
+ require.EqualError(t, err, "unable to parse JWT: token signature is invalid: signature is invalid")
+ require.Nil(t, claims)
}
func TestAuthorizeCookieNoContent(t *testing.T) {
- r, _ := http.NewRequest("GET", defaultHubURL, nil)
+ r, _ := http.NewRequest(http.MethodGet, defaultHubURL, nil)
r.AddCookie(&http.Cookie{Name: defaultCookieName, Value: validEmptyHeader})
- claims, err := authorize(r, &jwtConfig{[]byte("!ChangeMe!"), jwt.SigningMethodHS256}, []string{}, defaultCookieName)
- require.Nil(t, err)
- assert.Nil(t, claims.Mercure.Publish)
- assert.Nil(t, claims.Mercure.Subscribe)
+ claims, err := authorize(r, keyfuncHS256, []string{}, defaultCookieName)
+ require.NoError(t, err)
+ require.Nil(t, claims.Mercure.Publish)
+ require.Nil(t, claims.Mercure.Subscribe)
}
func TestAuthorizeCookieNoContentRsa(t *testing.T) {
- r, _ := http.NewRequest("GET", defaultHubURL, nil)
+ r, _ := http.NewRequest(http.MethodGet, defaultHubURL, nil)
r.AddCookie(&http.Cookie{Name: defaultCookieName, Value: validEmptyHeaderRsa})
- claims, err := authorize(r, &jwtConfig{[]byte(publicKeyRsa), jwt.SigningMethodRS256}, []string{}, defaultCookieName)
- require.Nil(t, err)
- assert.Nil(t, claims.Mercure.Publish)
- assert.Nil(t, claims.Mercure.Subscribe)
+ claims, err := authorize(r, keyfuncRS256publicKey, []string{}, defaultCookieName)
+ require.NoError(t, err)
+ require.Nil(t, claims.Mercure.Publish)
+ require.Nil(t, claims.Mercure.Subscribe)
}
func TestAuthorizeCookie(t *testing.T) {
- r, _ := http.NewRequest("GET", defaultHubURL, nil)
+ r, _ := http.NewRequest(http.MethodGet, defaultHubURL, nil)
r.AddCookie(&http.Cookie{Name: defaultCookieName, Value: validFullHeader})
- claims, err := authorize(r, &jwtConfig{[]byte("!ChangeMe!"), jwt.SigningMethodHS256}, []string{}, defaultCookieName)
- require.Nil(t, err)
+ claims, err := authorize(r, keyfuncHS256, []string{}, defaultCookieName)
+ require.NoError(t, err)
assert.Equal(t, []string{"foo", "bar"}, claims.Mercure.Publish)
assert.Equal(t, []string{"foo", "baz"}, claims.Mercure.Subscribe)
}
func TestAuthorizeCookieRsa(t *testing.T) {
- r, _ := http.NewRequest("GET", defaultHubURL, nil)
+ r, _ := http.NewRequest(http.MethodGet, defaultHubURL, nil)
r.AddCookie(&http.Cookie{Name: defaultCookieName, Value: validFullHeaderRsa})
- claims, err := authorize(r, &jwtConfig{[]byte(publicKeyRsa), jwt.SigningMethodRS256}, []string{}, defaultCookieName)
- require.Nil(t, err)
+ claims, err := authorize(r, keyfuncRS256publicKey, []string{}, defaultCookieName)
+ require.NoError(t, err)
assert.Equal(t, []string{"foo", "bar"}, claims.Mercure.Publish)
assert.Equal(t, []string{"foo", "baz"}, claims.Mercure.Subscribe)
}
func TestAuthorizeCookieNoOriginNoReferer(t *testing.T) {
- r, _ := http.NewRequest("POST", defaultHubURL, nil)
+ r, _ := http.NewRequest(http.MethodPost, defaultHubURL, nil)
r.AddCookie(&http.Cookie{Name: defaultCookieName, Value: validFullHeader})
- claims, err := authorize(r, &jwtConfig{[]byte("!ChangeMe!"), jwt.SigningMethodHS256}, []string{}, defaultCookieName)
- assert.EqualError(t, err, "an \"Origin\" or a \"Referer\" HTTP header must be present to use the cookie-based authorization mechanism")
- assert.Nil(t, claims)
+ claims, err := authorize(r, keyfuncHS256, []string{}, defaultCookieName)
+ require.EqualError(t, err, `an "Origin" or a "Referer" HTTP header must be present to use the cookie-based authorization mechanism`)
+ require.Nil(t, claims)
}
func TestAuthorizeCookieNoOriginNoRefererRsa(t *testing.T) {
- r, _ := http.NewRequest("POST", defaultHubURL, nil)
+ r, _ := http.NewRequest(http.MethodPost, defaultHubURL, nil)
r.AddCookie(&http.Cookie{Name: defaultCookieName, Value: validFullHeaderRsa})
- claims, err := authorize(r, &jwtConfig{[]byte(publicKeyRsa), jwt.SigningMethodRS256}, []string{}, defaultCookieName)
- assert.EqualError(t, err, "an \"Origin\" or a \"Referer\" HTTP header must be present to use the cookie-based authorization mechanism")
- assert.Nil(t, claims)
+ claims, err := authorize(r, keyfuncRS256publicKey, []string{}, defaultCookieName)
+ require.EqualError(t, err, `an "Origin" or a "Referer" HTTP header must be present to use the cookie-based authorization mechanism`)
+ require.Nil(t, claims)
}
func TestAuthorizeCookieOriginNotAllowed(t *testing.T) {
- r, _ := http.NewRequest("POST", defaultHubURL, nil)
+ r, _ := http.NewRequest(http.MethodPost, defaultHubURL, nil)
r.Header.Add("Origin", "http://example.com")
r.AddCookie(&http.Cookie{Name: defaultCookieName, Value: validFullHeader})
- claims, err := authorize(r, &jwtConfig{[]byte("!ChangeMe!"), jwt.SigningMethodHS256}, []string{"http://example.net"}, defaultCookieName)
- assert.EqualError(t, err, `"http://example.com": origin not allowed to post updates`)
- assert.Nil(t, claims)
+ claims, err := authorize(r, keyfuncHS256, []string{"http://example.net"}, defaultCookieName)
+ require.EqualError(t, err, `"http://example.com": origin not allowed to post updates`)
+ require.Nil(t, claims)
}
func TestAuthorizeCookieOriginNotAllowedRsa(t *testing.T) {
- r, _ := http.NewRequest("POST", defaultHubURL, nil)
+ r, _ := http.NewRequest(http.MethodPost, defaultHubURL, nil)
r.Header.Add("Origin", "http://example.com")
r.AddCookie(&http.Cookie{Name: defaultCookieName, Value: validFullHeaderRsa})
- claims, err := authorize(r, &jwtConfig{[]byte(publicKeyRsa), jwt.SigningMethodRS256}, []string{"http://example.net"}, defaultCookieName)
- assert.EqualError(t, err, `"http://example.com": origin not allowed to post updates`)
- assert.Nil(t, claims)
+ claims, err := authorize(r, keyfuncRS256publicKey, []string{"http://example.net"}, defaultCookieName)
+ require.EqualError(t, err, `"http://example.com": origin not allowed to post updates`)
+ require.Nil(t, claims)
}
func TestAuthorizeCookieRefererNotAllowed(t *testing.T) {
- r, _ := http.NewRequest("POST", defaultHubURL, nil)
+ r, _ := http.NewRequest(http.MethodPost, defaultHubURL, nil)
r.Header.Add("Referer", "http://example.com/foo/bar")
r.AddCookie(&http.Cookie{Name: defaultCookieName, Value: validFullHeader})
- claims, err := authorize(r, &jwtConfig{[]byte("!ChangeMe!"), jwt.SigningMethodHS256}, []string{"http://example.net"}, defaultCookieName)
- assert.EqualError(t, err, `"http://example.com": origin not allowed to post updates`)
- assert.Nil(t, claims)
+ claims, err := authorize(r, keyfuncHS256, []string{"http://example.net"}, defaultCookieName)
+ require.EqualError(t, err, `"http://example.com": origin not allowed to post updates`)
+ require.Nil(t, claims)
}
func TestAuthorizeCookieRefererNotAllowedRsa(t *testing.T) {
- r, _ := http.NewRequest("POST", defaultHubURL, nil)
+ r, _ := http.NewRequest(http.MethodPost, defaultHubURL, nil)
r.Header.Add("Referer", "http://example.com/foo/bar")
r.AddCookie(&http.Cookie{Name: defaultCookieName, Value: validFullHeaderRsa})
- claims, err := authorize(r, &jwtConfig{[]byte(publicKeyRsa), jwt.SigningMethodRS256}, []string{"http://example.net"}, defaultCookieName)
- assert.EqualError(t, err, `"http://example.com": origin not allowed to post updates`)
- assert.Nil(t, claims)
+ claims, err := authorize(r, keyfuncRS256publicKey, []string{"http://example.net"}, defaultCookieName)
+ require.EqualError(t, err, `"http://example.com": origin not allowed to post updates`)
+ require.Nil(t, claims)
}
func TestAuthorizeCookieInvalidReferer(t *testing.T) {
- r, _ := http.NewRequest("POST", defaultHubURL, nil)
+ r, _ := http.NewRequest(http.MethodPost, defaultHubURL, nil)
r.Header.Add("Referer", "http://192.168.0.%31/")
r.AddCookie(&http.Cookie{Name: defaultCookieName, Value: validFullHeader})
- claims, err := authorize(r, &jwtConfig{[]byte("!ChangeMe!"), jwt.SigningMethodHS256}, []string{"http://example.net"}, defaultCookieName)
- assert.EqualError(t, err, `unable to parse referer: parse "http://192.168.0.%31/": invalid URL escape "%31"`)
- assert.Nil(t, claims)
+ claims, err := authorize(r, keyfuncHS256, []string{"http://example.net"}, defaultCookieName)
+ require.EqualError(t, err, `unable to parse referer: parse "http://192.168.0.%31/": invalid URL escape "%31"`)
+ require.Nil(t, claims)
}
func TestAuthorizeCookieInvalidRefererRsa(t *testing.T) {
- r, _ := http.NewRequest("POST", defaultHubURL, nil)
+ r, _ := http.NewRequest(http.MethodPost, defaultHubURL, nil)
r.Header.Add("Referer", "http://192.168.0.%31/")
r.AddCookie(&http.Cookie{Name: defaultCookieName, Value: validFullHeaderRsa})
- claims, err := authorize(r, &jwtConfig{[]byte(publicKeyRsa), jwt.SigningMethodRS256}, []string{"http://example.net"}, defaultCookieName)
- assert.EqualError(t, err, `unable to parse referer: parse "http://192.168.0.%31/": invalid URL escape "%31"`)
- assert.Nil(t, claims)
+ claims, err := authorize(r, keyfuncRS256publicKey, []string{"http://example.net"}, defaultCookieName)
+ require.EqualError(t, err, `unable to parse referer: parse "http://192.168.0.%31/": invalid URL escape "%31"`)
+ require.Nil(t, claims)
}
func TestAuthorizeCookieOriginHasPriority(t *testing.T) {
- r, _ := http.NewRequest("POST", defaultHubURL, nil)
+ r, _ := http.NewRequest(http.MethodPost, defaultHubURL, nil)
r.Header.Add("Origin", "http://example.net")
r.Header.Add("Referer", "http://example.com")
r.AddCookie(&http.Cookie{Name: defaultCookieName, Value: validFullHeader})
- claims, err := authorize(r, &jwtConfig{[]byte("!ChangeMe!"), jwt.SigningMethodHS256}, []string{"http://example.net"}, defaultCookieName)
- require.Nil(t, err)
+ claims, err := authorize(r, keyfuncHS256, []string{"http://example.net"}, defaultCookieName)
+ require.NoError(t, err)
assert.Equal(t, []string{"foo", "bar"}, claims.Mercure.Publish)
assert.Equal(t, []string{"foo", "baz"}, claims.Mercure.Subscribe)
}
func TestAuthorizeCookieOriginHasPriorityRsa(t *testing.T) {
- r, _ := http.NewRequest("POST", defaultHubURL, nil)
+ r, _ := http.NewRequest(http.MethodPost, defaultHubURL, nil)
r.Header.Add("Origin", "http://example.net")
r.Header.Add("Referer", "http://example.com")
r.AddCookie(&http.Cookie{Name: defaultCookieName, Value: validFullHeaderRsa})
- claims, err := authorize(r, &jwtConfig{[]byte(publicKeyRsa), jwt.SigningMethodRS256}, []string{"http://example.net"}, defaultCookieName)
- require.Nil(t, err)
+ claims, err := authorize(r, keyfuncRS256publicKey, []string{"http://example.net"}, defaultCookieName)
+ require.NoError(t, err)
assert.Equal(t, []string{"foo", "bar"}, claims.Mercure.Publish)
assert.Equal(t, []string{"foo", "baz"}, claims.Mercure.Subscribe)
}
func TestAuthorizeAllOriginsAllowed(t *testing.T) {
- r, _ := http.NewRequest("POST", defaultHubURL, nil)
+ r, _ := http.NewRequest(http.MethodPost, defaultHubURL, nil)
r.Header.Add("Origin", "http://example.com")
r.AddCookie(&http.Cookie{Name: defaultCookieName, Value: validFullHeader})
- _, err := authorize(r, &jwtConfig{[]byte("!ChangeMe!"), jwt.SigningMethodHS256}, []string{"*"}, defaultCookieName)
- require.Nil(t, err)
+ _, err := authorize(r, keyfuncHS256, []string{"*"}, defaultCookieName)
+ require.NoError(t, err)
}
-func TestAuthorizeCustomCookieNane(t *testing.T) {
- r, _ := http.NewRequest("POST", defaultHubURL, nil)
+func TestAuthorizeCustomCookieName(t *testing.T) {
+ r, _ := http.NewRequest(http.MethodPost, defaultHubURL, nil)
r.Header.Add("Origin", "http://example.com")
r.AddCookie(&http.Cookie{Name: "foo", Value: validFullHeader})
- _, err := authorize(r, &jwtConfig{[]byte("!ChangeMe!"), jwt.SigningMethodHS256}, []string{"*"}, "foo")
- require.Nil(t, err)
+ _, err := authorize(r, keyfuncHS256, []string{"*"}, "foo")
+ require.NoError(t, err)
}
func TestCanReceive(t *testing.T) {
diff --git a/bolt_transport.go b/bolt_transport.go
index b2df9a92..2a55770e 100644
--- a/bolt_transport.go
+++ b/bolt_transport.go
@@ -39,7 +39,7 @@ type BoltTransport struct {
}
// NewBoltTransport create a new boltTransport.
-func NewBoltTransport(u *url.URL, l Logger, tss *TopicSelectorStore) (Transport, error) { //nolint:ireturn
+func NewBoltTransport(u *url.URL, l Logger) (Transport, error) { //nolint:ireturn
var err error
q := u.Query()
bucketName := defaultBoltBucketName
@@ -213,16 +213,10 @@ func (t *BoltTransport) GetSubscribers() (string, []*Subscriber, error) {
t.RLock()
defer t.RUnlock()
- var subscribers []*Subscriber
- t.subscribers.Walk(0, func(s *Subscriber) bool {
- subscribers = append(subscribers, s)
-
- return true
- })
-
- return t.lastEventID, subscribers, nil
+ return t.lastEventID, getSubscribers(t.subscribers), nil
}
+//nolint:gocognit
func (t *BoltTransport) dispatchHistory(s *Subscriber, toSeq uint64) {
t.db.View(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte(t.bucketName))
@@ -262,6 +256,11 @@ func (t *BoltTransport) dispatchHistory(s *Subscriber, toSeq uint64) {
}
}
s.HistoryDispatched(responseLastEventID)
+ if !afterFromID {
+ if c := t.logger.Check(zap.InfoLevel, "Can't find requested LastEventID"); c != nil {
+ c.Write(zap.String("LastEventID", s.RequestLastEventID))
+ }
+ }
return nil
})
diff --git a/bolt_transport_test.go b/bolt_transport_test.go
index 8f28c88b..5dfaca49 100644
--- a/bolt_transport_test.go
+++ b/bolt_transport_test.go
@@ -17,7 +17,7 @@ import (
func createBoltTransport(dsn string) *BoltTransport {
u, _ := url.Parse(dsn)
- transport, _ := NewBoltTransport(u, zap.NewNop(), nil)
+ transport, _ := NewBoltTransport(u, zap.NewNop())
return transport.(*BoltTransport)
}
@@ -38,7 +38,7 @@ func TestBoltTransportHistory(t *testing.T) {
s := NewSubscriber("8", transport.logger)
s.SetTopics(topics, nil)
- require.Nil(t, transport.AddSubscriber(s))
+ require.NoError(t, transport.AddSubscriber(s))
var count int
for {
@@ -52,6 +52,31 @@ func TestBoltTransportHistory(t *testing.T) {
}
}
+func TestBoltTransportLogsBogusLastEventID(t *testing.T) {
+ sink, logger := newTestLogger(t)
+ defer sink.Reset()
+
+ u, _ := url.Parse("bolt://test.db")
+ transport, _ := NewBoltTransport(u, logger)
+ defer transport.Close()
+ defer os.Remove("test.db")
+
+ // make sure the db is not empty
+ topics := []string{"https://example.com/foo"}
+ transport.Dispatch(&Update{
+ Event: Event{ID: "1"},
+ Topics: topics,
+ })
+
+ s := NewSubscriber("711131", logger)
+ s.SetTopics(topics, nil)
+
+ require.NoError(t, transport.AddSubscriber(s))
+
+ log := sink.String()
+ assert.Contains(t, log, `"LastEventID":"711131"`)
+}
+
func TestBoltTopicSelectorHistory(t *testing.T) {
transport := createBoltTransport("bolt://test.db")
defer transport.Close()
@@ -65,7 +90,7 @@ func TestBoltTopicSelectorHistory(t *testing.T) {
s := NewSubscriber(EarliestLastEventID, transport.logger)
s.SetTopics([]string{"http://example.com/subscribed", "http://example.com/subscribed-public-only"}, []string{"http://example.com/subscribed"})
- require.Nil(t, transport.AddSubscriber(s))
+ require.NoError(t, transport.AddSubscriber(s))
assert.Equal(t, "1", (<-s.Receive()).ID)
assert.Equal(t, "4", (<-s.Receive()).ID)
@@ -86,7 +111,7 @@ func TestBoltTransportRetrieveAllHistory(t *testing.T) {
s := NewSubscriber(EarliestLastEventID, transport.logger)
s.SetTopics(topics, nil)
- require.Nil(t, transport.AddSubscriber(s))
+ require.NoError(t, transport.AddSubscriber(s))
var count int
for {
@@ -116,7 +141,7 @@ func TestBoltTransportHistoryAndLive(t *testing.T) {
s := NewSubscriber("8", transport.logger)
s.SetTopics(topics, nil)
- require.Nil(t, transport.AddSubscriber(s))
+ require.NoError(t, transport.AddSubscriber(s))
var wg sync.WaitGroup
wg.Add(1)
@@ -166,28 +191,28 @@ func TestBoltTransportPurgeHistory(t *testing.T) {
func TestNewBoltTransport(t *testing.T) {
u, _ := url.Parse("bolt://test.db?bucket_name=demo")
- transport, err := NewBoltTransport(u, zap.NewNop(), nil)
- assert.Nil(t, err)
+ transport, err := NewBoltTransport(u, zap.NewNop())
+ require.NoError(t, err)
require.NotNil(t, transport)
transport.Close()
u, _ = url.Parse("bolt://")
- _, err = NewBoltTransport(u, zap.NewNop(), nil)
- assert.EqualError(t, err, `"bolt:": invalid transport: missing path`)
+ _, err = NewBoltTransport(u, zap.NewNop())
+ require.EqualError(t, err, `"bolt:": invalid transport: missing path`)
u, _ = url.Parse("bolt:///test.db")
- _, err = NewBoltTransport(u, zap.NewNop(), nil)
+ _, err = NewBoltTransport(u, zap.NewNop())
// The exact error message depends of the OS
assert.Contains(t, err.Error(), "open /test.db:")
u, _ = url.Parse("bolt://test.db?cleanup_frequency=invalid")
- _, err = NewBoltTransport(u, zap.NewNop(), nil)
- assert.EqualError(t, err, `"bolt://test.db?cleanup_frequency=invalid": invalid "cleanup_frequency" parameter "invalid": invalid transport: strconv.ParseFloat: parsing "invalid": invalid syntax`)
+ _, err = NewBoltTransport(u, zap.NewNop())
+ require.EqualError(t, err, `"bolt://test.db?cleanup_frequency=invalid": invalid "cleanup_frequency" parameter "invalid": invalid transport: strconv.ParseFloat: parsing "invalid": invalid syntax`)
u, _ = url.Parse("bolt://test.db?size=invalid")
- _, err = NewBoltTransport(u, zap.NewNop(), nil)
- assert.EqualError(t, err, `"bolt://test.db?size=invalid": invalid "size" parameter "invalid": invalid transport: strconv.ParseUint: parsing "invalid": invalid syntax`)
+ _, err = NewBoltTransport(u, zap.NewNop())
+ require.EqualError(t, err, `"bolt://test.db?size=invalid": invalid "size" parameter "invalid": invalid transport: strconv.ParseUint: parsing "invalid": invalid syntax`)
}
func TestBoltTransportDoNotDispatchUntilListen(t *testing.T) {
@@ -197,7 +222,7 @@ func TestBoltTransportDoNotDispatchUntilListen(t *testing.T) {
assert.Implements(t, (*Transport)(nil), transport)
s := NewSubscriber("", transport.logger)
- require.Nil(t, transport.AddSubscriber(s))
+ require.NoError(t, transport.AddSubscriber(s))
var wg sync.WaitGroup
wg.Add(1)
@@ -223,21 +248,21 @@ func TestBoltTransportDispatch(t *testing.T) {
s := NewSubscriber("", transport.logger)
s.SetTopics([]string{"https://example.com/foo", "https://example.com/private"}, []string{"https://example.com/private"})
- require.Nil(t, transport.AddSubscriber(s))
+ require.NoError(t, transport.AddSubscriber(s))
notSubscribed := &Update{Topics: []string{"not-subscribed"}}
- require.Nil(t, transport.Dispatch(notSubscribed))
+ require.NoError(t, transport.Dispatch(notSubscribed))
subscribedNotAuthorized := &Update{Topics: []string{"https://example.com/foo"}, Private: true}
- require.Nil(t, transport.Dispatch(subscribedNotAuthorized))
+ require.NoError(t, transport.Dispatch(subscribedNotAuthorized))
- public := &Update{Topics: s.Topics}
- require.Nil(t, transport.Dispatch(public))
+ public := &Update{Topics: s.SubscribedTopics}
+ require.NoError(t, transport.Dispatch(public))
assert.Equal(t, public, <-s.Receive())
- private := &Update{Topics: s.PrivateTopics, Private: true}
- require.Nil(t, transport.Dispatch(private))
+ private := &Update{Topics: s.AllowedPrivateTopics, Private: true}
+ require.NoError(t, transport.Dispatch(private))
assert.Equal(t, private, <-s.Receive())
}
@@ -251,12 +276,12 @@ func TestBoltTransportClosed(t *testing.T) {
s := NewSubscriber("", transport.logger)
s.SetTopics([]string{"https://example.com/foo"}, nil)
- require.Nil(t, transport.AddSubscriber(s))
+ require.NoError(t, transport.AddSubscriber(s))
- require.Nil(t, transport.Close())
- require.NotNil(t, transport.AddSubscriber(s))
+ require.NoError(t, transport.Close())
+ require.Error(t, transport.AddSubscriber(s))
- assert.Equal(t, transport.Dispatch(&Update{Topics: s.Topics}), ErrClosedTransport)
+ assert.Equal(t, transport.Dispatch(&Update{Topics: s.SubscribedTopics}), ErrClosedTransport)
_, ok := <-s.out
assert.False(t, ok)
@@ -270,11 +295,11 @@ func TestBoltCleanDisconnectedSubscribers(t *testing.T) {
s1 := NewSubscriber("", transport.logger)
s1.SetTopics([]string{"foo"}, []string{})
- require.Nil(t, transport.AddSubscriber(s1))
+ require.NoError(t, transport.AddSubscriber(s1))
s2 := NewSubscriber("", transport.logger)
s2.SetTopics([]string{"foo"}, []string{})
- require.Nil(t, transport.AddSubscriber(s2))
+ require.NoError(t, transport.AddSubscriber(s2))
assert.Equal(t, 2, transport.subscribers.Len())
@@ -294,30 +319,31 @@ func TestBoltGetSubscribers(t *testing.T) {
defer os.Remove("test.db")
s1 := NewSubscriber("", transport.logger)
- require.Nil(t, transport.AddSubscriber(s1))
+ require.NoError(t, transport.AddSubscriber(s1))
s2 := NewSubscriber("", transport.logger)
- require.Nil(t, transport.AddSubscriber(s2))
+ require.NoError(t, transport.AddSubscriber(s2))
lastEventID, subscribers, err := transport.GetSubscribers()
+ require.NoError(t, err)
+
assert.Equal(t, EarliestLastEventID, lastEventID)
assert.Len(t, subscribers, 2)
assert.Contains(t, subscribers, s1)
assert.Contains(t, subscribers, s2)
- assert.Nil(t, err)
}
func TestBoltLastEventID(t *testing.T) {
db, err := bolt.Open("test.db", 0o600, nil)
defer os.Remove("test.db")
- require.Nil(t, err)
+ require.NoError(t, err)
db.Update(func(tx *bolt.Tx) error {
bucket, err := tx.CreateBucketIfNotExists([]byte(defaultBoltBucketName))
- require.Nil(t, err)
+ require.NoError(t, err)
seq, err := bucket.NextSequence()
- require.Nil(t, err)
+ require.NoError(t, err)
prefix := make([]byte, 8)
binary.BigEndian.PutUint64(prefix, seq)
@@ -330,7 +356,7 @@ func TestBoltLastEventID(t *testing.T) {
return bucket.Put(key, []byte("invalid"))
})
- require.Nil(t, db.Close())
+ require.NoError(t, db.Close())
transport := createBoltTransport("bolt://test.db")
require.NotNil(t, transport)
diff --git a/caddy/caddy.go b/caddy/caddy.go
index 3bc522f3..2f6062a5 100644
--- a/caddy/caddy.go
+++ b/caddy/caddy.go
@@ -63,11 +63,11 @@ type Mercure struct {
// Maximum duration before closing the connection, defaults to 600s, set to 0 to disable.
WriteTimeout *caddy.Duration `json:"write_timeout,omitempty"`
- // Maximum dispatch duration of an update.
- DispatchTimeout caddy.Duration `json:"dispatch_timeout,omitempty"`
+ // Maximum dispatch duration of an update, defaults to 5s.
+ DispatchTimeout *caddy.Duration `json:"dispatch_timeout,omitempty"`
// Frequency of the heartbeat, defaults to 40s.
- Heartbeat caddy.Duration `json:"heartbeat,omitempty"`
+ Heartbeat *caddy.Duration `json:"heartbeat,omitempty"`
// JWT key and signing algorithm to use for publishers.
PublisherJWT JWTConfig `json:"publisher_jwt,omitempty"`
@@ -90,6 +90,9 @@ type Mercure struct {
// The name of the authorization cookie. Defaults to "mercureAuthorization".
CookieName string `json:"cookie_name,omitempty"`
+ // The version of the Mercure protocol to be backward compatible with (only version 7 is supported)
+ ProtocolVersionCompatibility int `json:"protocol_version_compatibility,omitempty"`
+
hub *mercure.Hub
logger *zap.Logger
}
@@ -137,13 +140,18 @@ func (m *Mercure) Provision(ctx caddy.Context) error { //nolint:funlen
return nil, fmt.Errorf("invalid transport url: %w", err)
}
- if m.WriteTimeout != nil {
- query := u.Query()
+ query := u.Query()
+ if m.WriteTimeout != nil && !query.Has("write_timeout") {
query.Set("write_timeout", time.Duration(*m.WriteTimeout).String())
- u.RawQuery = query.Encode()
}
- transport, err := mercure.NewTransport(u, m.logger, tss)
+ if m.Subscriptions && !query.Has("subscriptions") {
+ query.Set("subscriptions", "1")
+ }
+
+ u.RawQuery = query.Encode()
+
+ transport, err := mercure.NewTransport(u, m.logger)
if err != nil {
return nil, err //nolint:wrapcheck
}
@@ -193,11 +201,11 @@ func (m *Mercure) Provision(ctx caddy.Context) error { //nolint:funlen
if d := m.WriteTimeout; d != nil {
opts = append(opts, mercure.WithWriteTimeout(time.Duration(*d)))
}
- if d := m.DispatchTimeout; d != 0 {
- opts = append(opts, mercure.WithDispatchTimeout(time.Duration(d)))
+ if d := m.DispatchTimeout; d != nil {
+ opts = append(opts, mercure.WithDispatchTimeout(time.Duration(*d)))
}
- if d := m.Heartbeat; d != 0 {
- opts = append(opts, mercure.WithHeartbeat(time.Duration(d)))
+ if d := m.Heartbeat; d != nil {
+ opts = append(opts, mercure.WithHeartbeat(time.Duration(*d)))
}
if len(m.PublishOrigins) > 0 {
opts = append(opts, mercure.WithPublishOrigins(m.PublishOrigins))
@@ -205,6 +213,9 @@ func (m *Mercure) Provision(ctx caddy.Context) error { //nolint:funlen
if len(m.CORSOrigins) > 0 {
opts = append(opts, mercure.WithCORSOrigins(m.CORSOrigins))
}
+ if m.ProtocolVersionCompatibility != 0 {
+ opts = append(opts, mercure.WithProtocolVersionCompatibility(m.ProtocolVersionCompatibility))
+ }
h, err := mercure.NewHub(opts...)
if err != nil {
@@ -233,7 +244,7 @@ func (m Mercure) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhtt
}
// UnmarshalCaddyfile sets up the handler from Caddyfile tokens.
-func (m *Mercure) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { //nolint:funlen nolint:gocognit
+func (m *Mercure) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { //nolint:funlen,gocognit
for d.Next() {
for d.NextBlock(0) {
switch d.Val() {
@@ -272,7 +283,8 @@ func (m *Mercure) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { //nolint:fu
return err //nolint:wrapcheck
}
- m.DispatchTimeout = caddy.Duration(d)
+ cd := caddy.Duration(d)
+ m.DispatchTimeout = &cd
case "heartbeat":
if !d.NextArg() {
@@ -284,7 +296,8 @@ func (m *Mercure) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { //nolint:fu
return err //nolint:wrapcheck
}
- m.Heartbeat = caddy.Duration(d)
+ cd := caddy.Duration(d)
+ m.Heartbeat = &cd
case "publisher_jwt":
if !d.NextArg() {
@@ -347,6 +360,22 @@ func (m *Mercure) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { //nolint:fu
}
m.CookieName = d.Val()
+
+ case "protocol_version_compatibility":
+ if !d.NextArg() {
+ return d.ArgErr()
+ }
+
+ v, err := strconv.Atoi(d.Val())
+ if err != nil {
+ return err //nolint:wrapcheck
+ }
+
+ if v != 7 {
+ return errors.New("compatibility mode only supports protocol version 7")
+ }
+
+ m.ProtocolVersionCompatibility = v
}
}
}
diff --git a/caddy/caddy_test.go b/caddy/caddy_test.go
index 17eb4449..5564b4b9 100644
--- a/caddy/caddy_test.go
+++ b/caddy/caddy_test.go
@@ -4,7 +4,6 @@ import (
"context"
"errors"
"io"
- "io/ioutil"
"net/http"
"net/url"
"os"
@@ -17,6 +16,7 @@ import (
)
const (
+ bearerPrefix = "Bearer "
publisherJWT = "eyJhbGciOiJIUzI1NiJ9.eyJtZXJjdXJlIjp7InB1Ymxpc2giOlsiKiJdfX0.vhMwOaN5K68BTIhWokMLOeOJO4EPfT64brd8euJOA4M"
publisherJWTRSA = "eyJhbGciOiJSUzI1NiJ9.eyJtZXJjdXJlIjp7InB1Ymxpc2giOlsiKiJdLCJzdWJzY3JpYmUiOlsiaHR0cHM6Ly9leGFtcGxlLmNvbS9teS1wcml2YXRlLXRvcGljIiwie3NjaGVtZX06Ly97K2hvc3R9L2RlbW8vYm9va3Mve2lkfS5qc29ubGQiLCIvLndlbGwta25vd24vbWVyY3VyZS9zdWJzY3JpcHRpb25zey90b3BpY317L3N1YnNjcmliZXJ9Il0sInBheWxvYWQiOnsidXNlciI6Imh0dHBzOi8vZXhhbXBsZS5jb20vdXNlcnMvZHVuZ2xhcyIsInJlbW90ZUFkZHIiOiIxMjcuMC4wLjEifX19.iwryQ5k-CWNCNQLPg7CtgTdDWbG_CurSxDK8kMjTZfprGhh7Yli1SFt8WB3U4zbZ2wxUO7UfprZq3hnl8nSrozO9KDTCDwCYhMgRlcrdwm6XL1uXFwMJt4VSmp1srCQotv0FgT11jF8Km1vMQQOnUC27Va9fbfRtITVsjxsveYeMJqusVWO6F3vAvkM35oL8E8qgBbfrG_lnuhb_9Ws6RIq4YOslkOar_gopEs00CITxmV_aHVHRYzeW7QpycxjC7m8Mp-lKzaUewvJuKWI5HsM134xfaH8RAHSvh6H9pVQAiJ9tyc17bAx46M98WMsHFokVwz3rd7PoGGou6A7y5RzeGpiSxykTWCPPcBnxJ1gwUYqEYGTnRjl9JmhHY_VfQP4edyU-zhmMCCSie8rvkRDilAQGd5kj5m1voSn-EqA13sSe69evXxVUIB2nO70qHCcHBBHxunLqTIIerpc3F9_WWM4_Q_0j9CoTd2aFyuq_sdc6RcmAE3uTznp2DyKNQkT1EfpY7xCCe1MR-Webez5Ioa1EMDP0KrvLdnNRmuM3THSu1pqcvPV7Di7dJci5QWsYEmaP8cLuuZXdAhy_UoSgzbvfT_8mlDoJ9VvDXLJ39OwGYIyZiZ9VTNXm8mxre993cqg7boZRS8x70VRxnjmNxm40SgEvb6CHYO0lSBU"
subscriberJWT = "eyJhbGciOiJIUzI1NiJ9.eyJtZXJjdXJlIjp7InN1YnNjcmliZSI6WyIqIl19fQ.g3w81T7YQLKLrgovor9uEKUiOCAx6DmAAbq18qmDwsY"
@@ -26,9 +26,12 @@ func TestMercure(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`
{
+ skip_install_trust
+ admin localhost:2999
http_port 9080
https_port 9443
}
+
localhost:9080 {
route {
mercure {
@@ -48,7 +51,7 @@ func TestMercure(t *testing.T) {
go func() {
cx, cancel := context.WithCancel(context.Background())
- req, _ := http.NewRequest("GET", "http://localhost:9080/.well-known/mercure?topic=http%3A%2F%2Fexample.com%2Ffoo%2F1", nil)
+ req, _ := http.NewRequest(http.MethodGet, "http://localhost:9080/.well-known/mercure?topic=http%3A%2F%2Fexample.com%2Ffoo%2F1", nil)
req = req.WithContext(cx)
resp := tester.AssertResponseCode(req, http.StatusOK)
@@ -77,10 +80,10 @@ func TestMercure(t *testing.T) {
connected.Wait()
body := url.Values{"topic": {"http://example.com/foo/1"}, "data": {"bar"}, "id": {"bar"}}
- req, err := http.NewRequest("POST", "http://localhost:9080/.well-known/mercure", strings.NewReader(body.Encode()))
+ req, err := http.NewRequest(http.MethodPost, "http://localhost:9080/.well-known/mercure", strings.NewReader(body.Encode()))
require.Nil(t, err)
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
- req.Header.Add("Authorization", "Bearer "+publisherJWT)
+ req.Header.Add("Authorization", bearerPrefix+publisherJWT)
resp := tester.AssertResponseCode(req, http.StatusOK)
resp.Body.Close()
@@ -89,18 +92,19 @@ func TestMercure(t *testing.T) {
}
func TestJWTPlaceholders(t *testing.T) {
- k, _ := ioutil.ReadFile("../fixtures/jwt/RS256.key.pub")
- os.Setenv("TEST_JWT_KEY", string(k))
- defer os.Unsetenv("TEST_JWT_KEY")
- os.Setenv("TEST_JWT_ALG", "RS256")
- defer os.Unsetenv("TEST_JWT_ALG")
+ k, _ := os.ReadFile("../fixtures/jwt/RS256.key.pub")
+ t.Setenv("TEST_JWT_KEY", string(k))
+ t.Setenv("TEST_JWT_ALG", "RS256")
tester := caddytest.NewTester(t)
tester.InitServer(`
{
+ skip_install_trust
+ admin localhost:2999
http_port 9080
https_port 9443
}
+
localhost:9080 {
route {
mercure {
@@ -120,7 +124,7 @@ func TestJWTPlaceholders(t *testing.T) {
go func() {
cx, cancel := context.WithCancel(context.Background())
- req, _ := http.NewRequest("GET", "http://localhost:9080/.well-known/mercure?topic=http%3A%2F%2Fexample.com%2Ffoo%2F1", nil)
+ req, _ := http.NewRequest(http.MethodGet, "http://localhost:9080/.well-known/mercure?topic=http%3A%2F%2Fexample.com%2Ffoo%2F1", nil)
req = req.WithContext(cx)
resp := tester.AssertResponseCode(req, http.StatusOK)
@@ -149,10 +153,10 @@ func TestJWTPlaceholders(t *testing.T) {
connected.Wait()
body := url.Values{"topic": {"http://example.com/foo/1"}, "data": {"bar"}, "id": {"bar"}}
- req, err := http.NewRequest("POST", "http://localhost:9080/.well-known/mercure", strings.NewReader(body.Encode()))
+ req, err := http.NewRequest(http.MethodPost, "http://localhost:9080/.well-known/mercure", strings.NewReader(body.Encode()))
require.Nil(t, err)
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
- req.Header.Add("Authorization", "Bearer "+publisherJWTRSA)
+ req.Header.Add("Authorization", bearerPrefix+publisherJWTRSA)
resp := tester.AssertResponseCode(req, http.StatusOK)
resp.Body.Close()
@@ -164,9 +168,12 @@ func TestSubscriptionAPI(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`
{
+ skip_install_trust
+ admin localhost:2999
http_port 9080
https_port 9443
}
+
localhost:9080 {
route {
mercure {
@@ -180,7 +187,7 @@ func TestSubscriptionAPI(t *testing.T) {
}
`, "caddyfile")
- req, _ := http.NewRequest("GET", "http://localhost:9080/.well-known/mercure/subscriptions", nil)
+ req, _ := http.NewRequest(http.MethodGet, "http://localhost:9080/.well-known/mercure/subscriptions", nil)
resp := tester.AssertResponseCode(req, http.StatusOK)
resp.Body.Close()
}
@@ -189,6 +196,8 @@ func TestCookieName(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`
{
+ skip_install_trust
+ admin localhost:2999
http_port 9080
https_port 9443
}
@@ -213,7 +222,7 @@ func TestCookieName(t *testing.T) {
go func() {
cx, cancel := context.WithCancel(context.Background())
- req, _ := http.NewRequest("GET", "http://localhost:9080/.well-known/mercure?topic=http%3A%2F%2Fexample.com%2Ffoo%2F1", nil)
+ req, _ := http.NewRequest(http.MethodGet, "http://localhost:9080/.well-known/mercure?topic=http%3A%2F%2Fexample.com%2Ffoo%2F1", nil)
req.Header.Add("Origin", "http://localhost:9080")
req.AddCookie(&http.Cookie{Name: "foo", Value: subscriberJWT})
req = req.WithContext(cx)
@@ -244,7 +253,7 @@ func TestCookieName(t *testing.T) {
connected.Wait()
body := url.Values{"topic": {"http://example.com/foo/1"}, "data": {"bar"}, "id": {"bar"}, "private": {"1"}}
- req, err := http.NewRequest("POST", "http://localhost:9080/.well-known/mercure", strings.NewReader(body.Encode()))
+ req, err := http.NewRequest(http.MethodPost, "http://localhost:9080/.well-known/mercure", strings.NewReader(body.Encode()))
require.Nil(t, err)
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
req.Header.Add("Origin", "http://localhost:9080")
diff --git a/caddy/go.mod b/caddy/go.mod
index 50deb189..f3a12262 100644
--- a/caddy/go.mod
+++ b/caddy/go.mod
@@ -1,20 +1,188 @@
module github.com/dunglas/mercure/caddy
-go 1.16
+go 1.21
+
+toolchain go1.22.0
+
+retract (
+ v0.14.7 // CI problem
+ v0.14.6 // Overwriten tag
+)
replace github.com/dunglas/mercure => ../
require (
- github.com/caddyserver/caddy/v2 v2.4.3
- github.com/caddyserver/certmagic v0.14.1 // indirect
- github.com/fsnotify/fsnotify v1.5.1 // indirect
- github.com/dunglas/mercure v0.13.0
- github.com/google/uuid v1.3.0 // indirect
- github.com/klauspost/cpuid/v2 v2.0.9 // indirect
- github.com/miekg/dns v1.1.43 // indirect
- github.com/prometheus/client_golang v1.11.0
- github.com/smallstep/nosql v0.3.7 // indirect
- github.com/stretchr/testify v1.7.0
- go.uber.org/zap v1.19.0
- golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b // indirect
+ github.com/caddyserver/caddy/v2 v2.7.6
+ github.com/dunglas/mercure v0.15.11
+ github.com/prometheus/client_golang v1.19.0
+ github.com/stretchr/testify v1.9.0
+ go.uber.org/automaxprocs v1.5.3
+ go.uber.org/zap v1.27.0
+)
+
+require (
+ github.com/micromdm/scep/v2 v2.1.0 // indirect
+ github.com/smallstep/go-attestation v0.4.4-0.20240109183208-413678f90935 // indirect
+ go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352 // indirect
+)
+
+require (
+ filippo.io/edwards25519 v1.1.0 // indirect
+ github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 // indirect
+ github.com/BurntSushi/toml v1.3.2 // indirect
+ github.com/Masterminds/goutils v1.1.1 // indirect
+ github.com/Masterminds/semver/v3 v3.2.1 // indirect
+ github.com/Masterminds/sprig/v3 v3.2.3 // indirect
+ github.com/MauriceGit/skiplist v0.0.0-20211105230623-77f5c8d3e145 // indirect
+ github.com/Microsoft/go-winio v0.6.1 // indirect
+ github.com/RoaringBitmap/roaring v1.9.1 // indirect
+ github.com/alecthomas/chroma/v2 v2.9.1 // indirect
+ github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230305170008-8188dc5388df // indirect
+ github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b // indirect
+ github.com/beorn7/perks v1.0.1 // indirect
+ github.com/bits-and-blooms/bitset v1.13.0 // indirect
+ github.com/caddyserver/certmagic v0.20.0 // indirect
+ github.com/cenkalti/backoff/v4 v4.2.1 // indirect
+ github.com/cespare/xxhash v1.1.0 // indirect
+ github.com/cespare/xxhash/v2 v2.3.0 // indirect
+ github.com/chzyer/readline v1.5.1 // indirect
+ github.com/cpuguy83/go-md2man/v2 v2.0.3 // indirect
+ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
+ github.com/dgraph-io/badger v1.6.2 // indirect
+ github.com/dgraph-io/badger/v2 v2.2007.4 // indirect
+ github.com/dgraph-io/ristretto v0.1.1 // indirect
+ github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 // indirect
+ github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
+ github.com/dlclark/regexp2 v1.10.0 // indirect
+ github.com/dustin/go-humanize v1.0.1 // indirect
+ github.com/felixge/httpsnoop v1.0.4 // indirect
+ github.com/fsnotify/fsnotify v1.7.0 // indirect
+ github.com/fxamacker/cbor/v2 v2.5.0 // indirect
+ github.com/go-chi/chi/v5 v5.0.10 // indirect
+ github.com/go-kit/kit v0.13.0 // indirect
+ github.com/go-kit/log v0.2.1 // indirect
+ github.com/go-logfmt/logfmt v0.6.0 // indirect
+ github.com/go-logr/logr v1.4.1 // indirect
+ github.com/go-logr/stdr v1.2.2 // indirect
+ github.com/go-sql-driver/mysql v1.7.1 // indirect
+ github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
+ github.com/gofrs/uuid v4.4.0+incompatible // indirect
+ github.com/golang-jwt/jwt/v5 v5.2.1 // indirect
+ github.com/golang/glog v1.2.1 // indirect
+ github.com/golang/protobuf v1.5.3 // indirect
+ github.com/golang/snappy v0.0.4 // indirect
+ github.com/google/cel-go v0.15.1 // indirect
+ github.com/google/certificate-transparency-go v1.1.6 // indirect
+ github.com/google/go-tpm v0.9.0 // indirect
+ github.com/google/go-tspi v0.3.0 // indirect
+ github.com/google/pprof v0.0.0-20240402174815-29b9bb013b0f // indirect
+ github.com/google/uuid v1.6.0 // indirect
+ github.com/gorilla/handlers v1.5.2 // indirect
+ github.com/gorilla/mux v1.8.1 // indirect
+ github.com/grpc-ecosystem/grpc-gateway/v2 v2.18.0 // indirect
+ github.com/hashicorp/golang-lru v1.0.2 // indirect
+ github.com/hashicorp/hcl v1.0.0 // indirect
+ github.com/huandu/xstrings v1.4.0 // indirect
+ github.com/imdario/mergo v0.3.16 // indirect
+ github.com/inconshreveable/mousetrap v1.1.0 // indirect
+ github.com/jackc/chunkreader/v2 v2.0.1 // indirect
+ github.com/jackc/pgconn v1.14.1 // indirect
+ github.com/jackc/pgio v1.0.0 // indirect
+ github.com/jackc/pgpassfile v1.0.0 // indirect
+ github.com/jackc/pgproto3/v2 v2.3.2 // indirect
+ github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 // indirect
+ github.com/jackc/pgtype v1.14.1 // indirect
+ github.com/jackc/pgx/v4 v4.18.1 // indirect
+ github.com/kevburnsjr/skipfilter v0.0.1 // indirect
+ github.com/klauspost/compress v1.17.4 // indirect
+ github.com/klauspost/cpuid/v2 v2.2.6 // indirect
+ github.com/libdns/libdns v0.2.1 // indirect
+ github.com/magiconair/properties v1.8.7 // indirect
+ github.com/manifoldco/promptui v0.9.0 // indirect
+ github.com/mastercactapus/proxyprotocol v0.0.4 // indirect
+ github.com/mattn/go-colorable v0.1.13 // indirect
+ github.com/mattn/go-isatty v0.0.20 // indirect
+ github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
+ github.com/mholt/acmez v1.2.0 // indirect
+ github.com/miekg/dns v1.1.58 // indirect
+ github.com/mitchellh/copystructure v1.2.0 // indirect
+ github.com/mitchellh/go-ps v1.0.0 // indirect
+ github.com/mitchellh/mapstructure v1.5.0 // indirect
+ github.com/mitchellh/reflectwalk v1.0.2 // indirect
+ github.com/mschoch/smat v0.2.0 // indirect
+ github.com/onsi/ginkgo/v2 v2.17.1 // indirect
+ github.com/pelletier/go-toml/v2 v2.2.0 // indirect
+ github.com/pkg/errors v0.9.1 // indirect
+ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
+ github.com/prometheus/client_model v0.6.1 // indirect
+ github.com/prometheus/common v0.52.2 // indirect
+ github.com/prometheus/procfs v0.13.0 // indirect
+ github.com/quic-go/qpack v0.4.0 // indirect
+ github.com/quic-go/qtls-go1-20 v0.4.1 // indirect
+ github.com/quic-go/quic-go v0.40.1 // indirect
+ github.com/redis/go-redis/v9 v9.5.1 // indirect
+ github.com/rs/xid v1.5.0 // indirect
+ github.com/russross/blackfriday/v2 v2.1.0 // indirect
+ github.com/sagikazarmark/locafero v0.4.0 // indirect
+ github.com/sagikazarmark/slog-shim v0.1.0 // indirect
+ github.com/shopspring/decimal v1.3.1 // indirect
+ github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
+ github.com/sirupsen/logrus v1.9.3 // indirect
+ github.com/slackhq/nebula v1.8.2 // indirect
+ github.com/smallstep/certificates v0.25.0 // indirect
+ github.com/smallstep/nosql v0.6.0 // indirect
+ github.com/smallstep/truststore v0.12.1 // indirect
+ github.com/sourcegraph/conc v0.3.0 // indirect
+ github.com/spf13/afero v1.11.0 // indirect
+ github.com/spf13/cast v1.6.0 // indirect
+ github.com/spf13/cobra v1.8.0 // indirect
+ github.com/spf13/pflag v1.0.5 // indirect
+ github.com/spf13/viper v1.18.2 // indirect
+ github.com/stoewer/go-strcase v1.3.0 // indirect
+ github.com/subosito/gotenv v1.6.0 // indirect
+ github.com/tailscale/tscert v0.0.0-20230806124524-28a91b69a046 // indirect
+ github.com/unrolled/secure v1.14.0 // indirect
+ github.com/urfave/cli v1.22.14 // indirect
+ github.com/x448/float16 v0.8.4 // indirect
+ github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
+ github.com/yuin/goldmark v1.5.6 // indirect
+ github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc // indirect
+ github.com/zeebo/blake3 v0.2.3 // indirect
+ go.etcd.io/bbolt v1.3.9 // indirect
+ go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 // indirect
+ go.opentelemetry.io/contrib/propagators/autoprop v0.42.0 // indirect
+ go.opentelemetry.io/contrib/propagators/aws v1.17.0 // indirect
+ go.opentelemetry.io/contrib/propagators/b3 v1.17.0 // indirect
+ go.opentelemetry.io/contrib/propagators/jaeger v1.17.0 // indirect
+ go.opentelemetry.io/contrib/propagators/ot v1.17.0 // indirect
+ go.opentelemetry.io/otel v1.21.0 // indirect
+ go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0 // indirect
+ go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.21.0 // indirect
+ go.opentelemetry.io/otel/metric v1.21.0 // indirect
+ go.opentelemetry.io/otel/sdk v1.21.0 // indirect
+ go.opentelemetry.io/otel/trace v1.21.0 // indirect
+ go.opentelemetry.io/proto/otlp v1.0.0 // indirect
+ go.step.sm/cli-utils v0.8.0 // indirect
+ go.step.sm/crypto v0.35.1 // indirect
+ go.step.sm/linkedca v0.20.1 // indirect
+ go.uber.org/mock v0.4.0 // indirect
+ go.uber.org/multierr v1.11.0 // indirect
+ golang.org/x/crypto v0.22.0 // indirect
+ golang.org/x/exp v0.0.0-20240404231335-c0f41cb1a7a0 // indirect
+ golang.org/x/mod v0.17.0 // indirect
+ golang.org/x/net v0.24.0 // indirect
+ golang.org/x/sync v0.7.0 // indirect
+ golang.org/x/sys v0.19.0 // indirect
+ golang.org/x/term v0.19.0 // indirect
+ golang.org/x/text v0.14.0 // indirect
+ golang.org/x/tools v0.20.0 // indirect
+ google.golang.org/genproto/googleapis/api v0.0.0-20240123012728-ef4313101c80 // indirect
+ google.golang.org/genproto/googleapis/rpc v0.0.0-20240123012728-ef4313101c80 // indirect
+ google.golang.org/grpc v1.61.0 // indirect
+ google.golang.org/protobuf v1.33.0 // indirect
+ gopkg.in/ini.v1 v1.67.0 // indirect
+ gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
+ gopkg.in/square/go-jose.v2 v2.6.0 // indirect
+ gopkg.in/yaml.v3 v3.0.1 // indirect
+ howett.net/plist v1.0.1 // indirect
)
diff --git a/caddy/go.sum b/caddy/go.sum
index 939942c7..190f3740 100644
--- a/caddy/go.sum
+++ b/caddy/go.sum
@@ -1,1365 +1,684 @@
-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.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
-cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
-cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
-cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
-cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
-cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
-cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
-cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
-cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
-cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
-cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
-cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
-cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
-cloud.google.com/go v0.70.0/go.mod h1:/UTKYRQTWjVnSe7nGvoSzxEFUELzSI/yAYd0JQT6cRo=
-cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI=
-cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk=
-cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg=
-cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8=
-cloud.google.com/go v0.81.0 h1:at8Tk2zUz63cLPR0JPWm5vp77pEZmzxEQBEfRKn1VV8=
-cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0=
-cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
-cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
-cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
-cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
-cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
-cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
-cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
-cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
-cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk=
-cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
-cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
-cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
-cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
-cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
-cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
-cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
-cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
-cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
-contrib.go.opencensus.io/exporter/stackdriver v0.12.1/go.mod h1:iwB6wGarfphGGe/e5CWqyUk/cLzKnWsOKPVW3no6OTw=
-contrib.go.opencensus.io/resource v0.1.1/go.mod h1:F361eGI91LCmW1I/Saf+rX0+OFcigGlFvXwEGEnkRLA=
-dmitri.shuralyov.com/app/changes v0.0.0-20180602232624-0a106ad413e3/go.mod h1:Yl+fi1br7+Rr3LqpNJf1/uxUdtRUV+Tnj0o93V2B9MU=
-dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
-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=
-git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg=
+cloud.google.com/go v0.110.10 h1:LXy9GEO+timppncPIAZoOj3l58LIU9k+kn48AN7IO3Y=
+cloud.google.com/go/compute v1.23.3 h1:6sVlXXBmbd7jNX0Ipq0trII3e4n1/MsADLK6a+aiVlk=
+cloud.google.com/go/compute v1.23.3/go.mod h1:VCgBUoMnIVIR0CscqQiPJLAG25E3ZRZMzcFZeQ+h8CI=
+cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY=
+cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA=
+cloud.google.com/go/iam v1.1.5 h1:1jTsCu4bcsNsE4iiqNT5SHwrDRCfRmIaaaVFhRveTJI=
+cloud.google.com/go/iam v1.1.5/go.mod h1:rB6P/Ic3mykPbFio+vo7403drjlgvoWfYpJhMXEbzv8=
+cloud.google.com/go/kms v1.15.2 h1:lh6qra6oC4AyWe5fUUUBe/S27k12OHAleOOOw6KakdE=
+cloud.google.com/go/kms v1.15.2/go.mod h1:3hopT4+7ooWRCjc2DxgnpESFxhIraaI2IpAVUEhbT/w=
+filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
+filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 h1:cTp8I5+VIoKjsnZuH8vjyaysT/ses3EvZeaV/1UkF2M=
github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8=
-github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
-github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
-github.com/DataDog/zstd v1.4.1/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo=
-github.com/DataDog/zstd v1.4.5 h1:EndNeuB0l9syBZhut0wns3gV1hL8zX8LIu6ZiVHWLIQ=
-github.com/DataDog/zstd v1.4.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo=
-github.com/Masterminds/glide v0.13.2/go.mod h1:STyF5vcenH/rUqTEv+/hBXlSTo7KYwg2oc2f4tzPWic=
-github.com/Masterminds/goutils v1.1.0/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
+github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8=
+github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
-github.com/Masterminds/semver v1.4.2 h1:WBLTQ37jOCzSLtXNdoo8bNM8876KhNqOKvrlGITgsTc=
-github.com/Masterminds/semver v1.4.2/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
-github.com/Masterminds/semver/v3 v3.1.0/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
-github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc=
github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
-github.com/Masterminds/sprig/v3 v3.1.0/go.mod h1:ONGMf7UfYGAbMXCZmQLy8x3lCDIPrEZE/rU8pmrbihA=
-github.com/Masterminds/sprig/v3 v3.2.2 h1:17jRggJu518dr3QaafizSXOjKYp94wKfABxUmyxvxX8=
-github.com/Masterminds/sprig/v3 v3.2.2/go.mod h1:UoaO7Yp8KlPnJIYWTFkMaqPUYKTfGFPhxNuwnnxkKlk=
-github.com/Masterminds/vcs v1.13.0/go.mod h1:N09YCmOQr6RLxC6UNHzuVwAdodYbbnycGHSmwVJjcKA=
-github.com/MauriceGit/skiplist v0.0.0-20191117202105-643e379adb62 h1:gDG99+yxKg62ymqQVT33Vb2oDaxTx5j3M87VdR8xcOs=
+github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
+github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0=
+github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
+github.com/Masterminds/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj9n6YA=
+github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBaRMhvYXJNkGuM=
github.com/MauriceGit/skiplist v0.0.0-20191117202105-643e379adb62/go.mod h1:877WBceefKn14QwVVn4xRFUsHsZb9clICgdeTj4XsUg=
-github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA=
+github.com/MauriceGit/skiplist v0.0.0-20211105230623-77f5c8d3e145 h1:1yw6O62BReQ+uA1oyk9XaQTvLhcoHWmoQAgXmDFXpIY=
+github.com/MauriceGit/skiplist v0.0.0-20211105230623-77f5c8d3e145/go.mod h1:877WBceefKn14QwVVn4xRFUsHsZb9clICgdeTj4XsUg=
+github.com/Microsoft/go-winio v0.6.0/go.mod h1:cTAf44im0RAYeL23bpB+fzCyDH2MJiz2BO69KH/soAE=
+github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow=
+github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
-github.com/OpenPeeDeeP/depguard v1.0.0/go.mod h1:7/4sitnI9YlQgTLLk734QlzXT8DuHVnAyztLplQjk+o=
-github.com/OpenPeeDeeP/depguard v1.0.1/go.mod h1:xsIw86fROiiwelg+jB2uM9PiKihMMmUx/1V+TNhjQvM=
-github.com/RoaringBitmap/roaring v0.9.4 h1:ckvZSX5gwCRaJYBNe7syNawCU5oruY9gQmjXlp4riwo=
github.com/RoaringBitmap/roaring v0.9.4/go.mod h1:icnadbWcNyfEHlYdr+tDlOTih1Bf/h+rzPpv4sbomAA=
-github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg=
-github.com/ThalesIgnite/crypto11 v1.2.4/go.mod h1:ILDKtnCKiQ7zRoNxcp36Y1ZR8LBPmR2E23+wTQe/MlE=
-github.com/ThomasRooney/gexpect v0.0.0-20161231170123-5482f0350944/go.mod h1:sPML5WwI6oxLRLPuuqbtoOKhtmpVDCYtwsps+I+vjIY=
-github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38 h1:smF2tmSOzy2Mm+0dGI2AIUHY+w0BUc+4tn40djz7+6U=
-github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38/go.mod h1:r7bzyVFMNntcxPZXK3/+KdruV1H5KSlyVY0gc+NgInI=
-github.com/alecthomas/chroma v0.7.2-0.20200305040604-4f3623dce67a/go.mod h1:fv5SzZPFJbwp2NXJWpFIX7DZS4HgV1K4ew4Pc2OZD9s=
-github.com/alecthomas/chroma v0.9.1 h1:cBmvQqRImzR5aWqdMxYZByND4S7BCS/g0svZb28h0Dc=
-github.com/alecthomas/chroma v0.9.1/go.mod h1:eMuEnpA18XbG/WhOWtCzJHS7WqEtDAI+HxdwoW0nVSk=
-github.com/alecthomas/colour v0.0.0-20160524082231-60882d9e2721 h1:JHZL0hZKJ1VENNfmXvHbgYlbUOvpzYzvy2aZU5gXVeo=
-github.com/alecthomas/colour v0.0.0-20160524082231-60882d9e2721/go.mod h1:QO9JBoKquHd+jz9nshCh40fOfO+JzsoXy8qTHF68zU0=
-github.com/alecthomas/kong v0.2.1-0.20190708041108-0548c6b1afae/go.mod h1:+inYUSluD+p4L8KdviBSgzcqEjUQOfC5fQDRFuc36lI=
-github.com/alecthomas/kong v0.2.4/go.mod h1:kQOmtJgV+Lb4aj+I2LEn40cbtawdWJ9Y8QLq+lElKxE=
-github.com/alecthomas/repr v0.0.0-20180818092828-117648cd9897 h1:p9Sln00KOTlrYkxI1zYWl1QLnEqAqEARBEYa8FQnQcY=
-github.com/alecthomas/repr v0.0.0-20180818092828-117648cd9897/go.mod h1:xTS7Pm1pD1mvyM075QCDSRqH6qRLXylzS24ZTpRiSzQ=
-github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
-github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
-github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
-github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
-github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
-github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
-github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
-github.com/antlr/antlr4 v0.0.0-20200503195918-621b933c7a7f h1:0cEys61Sr2hUBEXfNV8eyQP01oZuBgoMeHunebPirK8=
-github.com/antlr/antlr4 v0.0.0-20200503195918-621b933c7a7f/go.mod h1:T7PbCXFs94rrTttyxjbyT5+/1V8T2TYDejxUfHJjw1Y=
-github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
+github.com/RoaringBitmap/roaring v1.9.1 h1:LXcSqGGGMKm+KAzUyWn7ZeREqoOkoMX+KwLOK1thc4I=
+github.com/RoaringBitmap/roaring v1.9.1/go.mod h1:6AXUsoIEzDTFFQCe1RbGA6uFONMhvejWj5rqITANK90=
+github.com/alecthomas/assert/v2 v2.2.1 h1:XivOgYcduV98QCahG8T5XTezV5bylXe+lBxLG2K2ink=
+github.com/alecthomas/assert/v2 v2.2.1/go.mod h1:pXcQ2Asjp247dahGEmsZ6ru0UVwnkhktn7S0bBDLxvQ=
+github.com/alecthomas/chroma/v2 v2.2.0/go.mod h1:vf4zrexSH54oEjJ7EdB65tGNHmH3pGZmVkgTP5RHvAs=
+github.com/alecthomas/chroma/v2 v2.9.1 h1:0O3lTQh9FxazJ4BYE/MOi/vDGuHn7B+6Bu902N2UZvU=
+github.com/alecthomas/chroma/v2 v2.9.1/go.mod h1:4TQu7gdfuPjSh76j78ietmqh9LiurGF0EpseFXdKMBw=
+github.com/alecthomas/repr v0.0.0-20220113201626-b1b626ac65ae/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8=
+github.com/alecthomas/repr v0.2.0 h1:HAzS41CIzNW5syS8Mf9UwXhNH1J9aix/BvDRf1Ml2Yk=
+github.com/alecthomas/repr v0.2.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
+github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230305170008-8188dc5388df h1:7RFfzj4SSt6nnvCPbCqijJi1nWCd+TqAT3bYCStRC18=
+github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230305170008-8188dc5388df/go.mod h1:pSwJ0fSY5KhvocuWSx4fz3BA8OrA1bQn+K1Eli3BRwM=
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
-github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
-github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b h1:uUXgbcPDK3KpW29o4iy7GtuappbWT0l5NaMo9H9pJDw=
github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A=
-github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
-github.com/aws/aws-sdk-go v1.19.18/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
-github.com/aws/aws-sdk-go v1.30.29 h1:NXNqBS9hjOCpDL8SyCyl38gZX3LLLunKOJc5E7vJ8P0=
-github.com/aws/aws-sdk-go v1.30.29/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0=
-github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
-github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
-github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
-github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
+github.com/aws/aws-sdk-go v1.45.12 h1:+bKbbesGNPp+TeGrcqfrWuZoqcIEhjwKyBMHQPp80Jo=
+github.com/aws/aws-sdk-go v1.45.12/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI=
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/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
-github.com/bits-and-blooms/bitset v1.2.0 h1:Kn4yilvwNtMACtf1eYDlG8H77R07mZSPbMjLyS07ChA=
github.com/bits-and-blooms/bitset v1.2.0/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edYb8uY+O0FJTyyDA=
-github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84=
-github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM=
-github.com/bombsimon/wsl/v2 v2.0.0/go.mod h1:mf25kr/SqFEPhhcxW1+7pxzGlW+hIl/hYTKY95VwV8U=
-github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
-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/caddyserver/caddy/v2 v2.4.3 h1:Y1FaV2N4WO3rBqxSYA8UZsZTQdN+PwcoOcAiZTM8C0I=
-github.com/caddyserver/caddy/v2 v2.4.3/go.mod h1:kyVbYM0adP2O8zl7hrkwYh2V7nQxDQRyXh0AEi30L5g=
-github.com/caddyserver/certmagic v0.14.0/go.mod h1:oRQOZmUVKwlpgNidslysHt05osM9uMrJ4YMk+Ot4P4Q=
-github.com/caddyserver/certmagic v0.14.1 h1:8RIFS/LbGne/I7Op56Kkm2annnei7io9VW/IWDttE9U=
-github.com/caddyserver/certmagic v0.14.1/go.mod h1:oRQOZmUVKwlpgNidslysHt05osM9uMrJ4YMk+Ot4P4Q=
-github.com/census-instrumentation/opencensus-proto v0.2.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
-github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
+github.com/bits-and-blooms/bitset v1.12.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
+github.com/bits-and-blooms/bitset v1.13.0 h1:bAQ9OPNFYbGHV6Nez0tmNI0RiEu7/hxlYJRUA0wFAVE=
+github.com/bits-and-blooms/bitset v1.13.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
+github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps=
+github.com/caddyserver/caddy/v2 v2.7.6 h1:w0NymbG2m9PcvKWsrXO6EEkY9Ru4FJK8uQbYcev1p3A=
+github.com/caddyserver/caddy/v2 v2.7.6/go.mod h1:JCiwFMnRWjk8lOa7po0wM/75kwd38ccJPMSrXvQCMQ0=
+github.com/caddyserver/certmagic v0.20.0 h1:bTw7LcEZAh9ucYCRXyCpIrSAGplplI0vGYJ4BpCQ/Fc=
+github.com/caddyserver/certmagic v0.20.0/go.mod h1:N4sXgpICQUskEWpj7zVzvWD41p3NYacrNoZYiRM2jTg=
+github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM=
+github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
-github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
-github.com/cheekybits/genny v1.0.0 h1:uGGa4nei+j20rOSeDeP5Of12XVm7TGUd4dJA9RDitfE=
-github.com/cheekybits/genny v1.0.0/go.mod h1:+tQajlRqAUrPI7DOSpB0XAqZYtQakVtB7wXkRAgjxjQ=
-github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE=
+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/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
-github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8=
+github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM=
+github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
-github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8=
+github.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI=
+github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
-github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
-github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
-github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
-github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
-github.com/codegangsta/cli v1.20.0/go.mod h1:/qJNoX69yVSKu5o4jLyXAENLRyk1uhi7zkbQ3slBdOA=
-github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
+github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04=
+github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8=
+github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I=
+github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
-github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
-github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
-github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
-github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
-github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
-github.com/corpix/uarand v0.1.1/go.mod h1:SFKZvkcRoLqVRFZ4u25xPmp6m9ktANfbpXZ7SJ0/FNU=
-github.com/cpuguy83/go-md2man v1.0.10 h1:BSKMNlYxDvnunlTymqtgONjNnaRV1sTpcovwwjF22jk=
+github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
-github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
-github.com/cpuguy83/go-md2man/v2 v2.0.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng4PGlyM=
-github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
+github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
+github.com/cpuguy83/go-md2man/v2 v2.0.3 h1:qMCsGGgs+MAzDFyp9LpAe1Lqy/fY/qCovCm0qnXZOBM=
+github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
-github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
-github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964 h1:y5HC9v93H5EPKqaS1UYVg1uYah5Xf51mBfIoWehClUQ=
-github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964/go.mod h1:Xd9hchkHSWYkEqJwUGisez3G1QY8Ryz0sdWrLPMGjLk=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
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/dgraph-io/badger v1.6.2 h1:mNw0qs90GVgGGWylh0umH5iag1j6n/PeJtNvL6KY/x8=
github.com/dgraph-io/badger v1.6.2/go.mod h1:JW2yswe3V058sS0kZ2h/AXeDSqFjxnZcRrVH//y2UQE=
-github.com/dgraph-io/badger/v2 v2.0.1-rc1.0.20201003150343-5d1bab4fc658/go.mod h1:2uGEvGm+JSDLd5UAaKIFSbXDcYyeH0fWJP4N2HMMYMI=
-github.com/dgraph-io/badger/v2 v2.2007.3 h1:Sl9tQWz92WCbVSe8pj04Tkqlm2boW+KAxd+XSs58SQI=
-github.com/dgraph-io/badger/v2 v2.2007.3/go.mod h1:26P/7fbL4kUZVEVKLAKXkBXKOydDmM2p1e+NhhnBCAE=
+github.com/dgraph-io/badger/v2 v2.2007.4 h1:TRWBQg8UrlUhaFdco01nO2uXwzKS7zd+HVdwV/GHc4o=
+github.com/dgraph-io/badger/v2 v2.2007.4/go.mod h1:vSw/ax2qojzbN6eXHIx6KPKtCSHJN/Uz0X0VPruTIhk=
github.com/dgraph-io/ristretto v0.0.2/go.mod h1:KPxhHT9ZxKefz+PCeOGsrHpl1qZ7i70dGTu2u+Ahh6E=
github.com/dgraph-io/ristretto v0.0.3-0.20200630154024-f66de99634de/go.mod h1:KPxhHT9ZxKefz+PCeOGsrHpl1qZ7i70dGTu2u+Ahh6E=
-github.com/dgraph-io/ristretto v0.0.4-0.20200906165740-41ebdbffecfd/go.mod h1:YylP9MpCYGVZQrly/j/diqcdUetCRRePeBB0c2VGXsA=
-github.com/dgraph-io/ristretto v0.1.0 h1:Jv3CGQHp9OjuMBSne1485aDpUkTKEcUqF+jm/LuerPI=
-github.com/dgraph-io/ristretto v0.1.0/go.mod h1:fux0lOrBhrVCJd3lcTHsIJhq1T2rokOu6v9Vcb3Q9ug=
-github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
+github.com/dgraph-io/ristretto v0.1.1 h1:6CWw5tJNgpegArSHpNHJKldNeq03FQCwYvfMVWajOK8=
+github.com/dgraph-io/ristretto v0.1.1/go.mod h1:S1GPSBCYCIhmVNfcth17y2zZtQT6wzkzgwUve0VDWWA=
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 h1:fAjc9m62+UWV/WAFKLNi6ZS0675eEUC9y3AlwSbQu1Y=
github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
-github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
-github.com/dlclark/regexp2 v1.1.6/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
-github.com/dlclark/regexp2 v1.2.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
-github.com/dlclark/regexp2 v1.4.0 h1:F1rxgk7p4uKjwIQxBs9oAXe5CqrXlCduYEJvrF4u93E=
github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
+github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
+github.com/dlclark/regexp2 v1.10.0 h1:+/GIL799phkJqYW+3YbOd8LCcbHzT0Pbo8zl70MHsq0=
+github.com/dlclark/regexp2 v1.10.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
-github.com/dustin/go-humanize v1.0.1-0.20200219035652-afde56e7acac h1:opbrjaN/L8gg6Xh5D04Tem+8xVcz6ajZlGCs49mQgyg=
-github.com/dustin/go-humanize v1.0.1-0.20200219035652-afde56e7acac/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
-github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
-github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
-github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
-github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
-github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
-github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
-github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
-github.com/fatih/color v1.6.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
-github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
-github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
-github.com/felixge/httpsnoop v1.0.2 h1:+nS9g82KMXccJ/wp0zyRW9ZBHFETmMGtkk+2CTTrW4o=
-github.com/felixge/httpsnoop v1.0.2/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/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY=
+github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
+github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
+github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
+github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
+github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
+github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
-github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
-github.com/fsnotify/fsnotify v1.5.0 h1:NO5hkcB+srp1x6QmwvNZLeaOgbM8cmBTN32THzjvu2k=
-github.com/fsnotify/fsnotify v1.5.0/go.mod h1:BX0DCEr5pT4jm2CnQdVP1lFV521fcCNcyEeNp4DQQDk=
-github.com/fsnotify/fsnotify v1.5.1 h1:mZcQUHVQUQWoPXXtuf9yuEXKudkV2sx1E06UadKWpgI=
-github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU=
-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-chi/chi v4.0.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
-github.com/go-chi/chi v4.1.2+incompatible h1:fGFk2Gmi/YKXk0OmGfBh0WgmN3XB8lVnEyNz34tQRec=
-github.com/go-chi/chi v4.1.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
-github.com/go-critic/go-critic v0.3.5-0.20190526074819-1df300866540/go.mod h1:+sE8vrLDS2M0pZkBk0wy6+nLdKexVDrl/jBqQOTDThA=
-github.com/go-critic/go-critic v0.4.1/go.mod h1:7/14rZGnZbY6E38VEGk2kVhoq6itzc1E68facVDK23g=
-github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
-github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
-github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
-github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
-github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
-github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
+github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
+github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
+github.com/fxamacker/cbor/v2 v2.5.0 h1:oHsG0V/Q6E/wqTS2O1Cozzsy69nqCiguo5Q1a1ADivE=
+github.com/fxamacker/cbor/v2 v2.5.0/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo=
+github.com/go-chi/chi/v5 v5.0.10 h1:rLz5avzKpjqxrYwXNfmjkrYYXOyLJd37pz53UFHC6vk=
+github.com/go-chi/chi/v5 v5.0.10/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
+github.com/go-kit/kit v0.4.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
+github.com/go-kit/kit v0.13.0 h1:OoneCcHKHQ03LfBpoQCUfCluwd2Vt3ohz+kvbJneZAU=
+github.com/go-kit/kit v0.13.0/go.mod h1:phqEHMMUbyrCFCTgH48JueqrM3md2HcAZ8N3XE4FKDg=
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
-github.com/go-lintpack/lintpack v0.5.2/go.mod h1:NwZuYi2nUHho8XEIZ6SIxihrnPoqBTDqfpXvXAN0sXM=
+github.com/go-kit/log v0.2.1 h1:MRVx0/zhvdseW+Gza6N9rVzU/IVzaeE1SFI4raAhmBU=
+github.com/go-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
-github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
-github.com/go-ole/go-ole v1.2.1/go.mod h1:7FAglXiTm7HKlQRDeOQ6ZNUHidzCWXuZWq/1dTyBNF8=
-github.com/go-piv/piv-go v1.7.0/go.mod h1:ON2WvQncm7dIkCQ7kYJs+nc3V4jHGfrrJnSF8HKy7Gk=
-github.com/go-redis/redis/v8 v8.11.1 h1:Aqf/1y2eVfE9zrySM++/efzwv3mkLH7n/T96//gbo94=
-github.com/go-redis/redis/v8 v8.11.1/go.mod h1:DLomh7y2e3ggQXQLd1YgmvIfecPJoFl7WU5SOQ/r06M=
-github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
-github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
-github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs=
-github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
+github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4=
+github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
+github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
+github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
+github.com/go-logr/logr v1.4.1/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/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI=
+github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
+github.com/go-stack/stack v1.6.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
-github.com/go-toolsmith/astcast v1.0.0/go.mod h1:mt2OdQTeAQcY4DQgPSArJjHCcOwlX+Wl/kwN+LbLGQ4=
-github.com/go-toolsmith/astcopy v1.0.0/go.mod h1:vrgyG+5Bxrnz4MZWPF+pI4R8h3qKRjjyvV/DSez4WVQ=
-github.com/go-toolsmith/astequal v0.0.0-20180903214952-dcb477bfacd6/go.mod h1:H+xSiq0+LtiDC11+h1G32h7Of5O3CYFJ99GVbS5lDKY=
-github.com/go-toolsmith/astequal v1.0.0/go.mod h1:H+xSiq0+LtiDC11+h1G32h7Of5O3CYFJ99GVbS5lDKY=
-github.com/go-toolsmith/astfmt v0.0.0-20180903215011-8f8ee99c3086/go.mod h1:mP93XdblcopXwlyN4X4uodxXQhldPGZbcEJIimQHrkg=
-github.com/go-toolsmith/astfmt v1.0.0/go.mod h1:cnWmsOAuq4jJY6Ct5YWlVLmcmLMn1JUPuQIHCY7CJDw=
-github.com/go-toolsmith/astinfo v0.0.0-20180906194353-9809ff7efb21/go.mod h1:dDStQCHtmZpYOmjRP/8gHHnCCch3Zz3oEgCdZVdtweU=
-github.com/go-toolsmith/astp v0.0.0-20180903215135-0af7e3c24f30/go.mod h1:SV2ur98SGypH1UjcPpCatrV5hPazG6+IfNHbkDXBRrk=
-github.com/go-toolsmith/astp v1.0.0/go.mod h1:RSyrtpVlfTFGDYRbrjyWP1pYu//tSFcvdYrA8meBmLI=
-github.com/go-toolsmith/pkgload v0.0.0-20181119091011-e9e65178eee8/go.mod h1:WoMrjiy4zvdS+Bg6z9jZH82QXwkcgCBX6nOfnmdaHks=
-github.com/go-toolsmith/pkgload v1.0.0/go.mod h1:5eFArkbO80v7Z0kdngIxsRXRMTaX4Ilcwuh3clNrQJc=
-github.com/go-toolsmith/strparse v1.0.0/go.mod h1:YI2nUKP9YGZnL/L1/DLFBfixrcjslWct4wyljWhSRy8=
-github.com/go-toolsmith/typep v1.0.0/go.mod h1:JSQCQMUPdRlMZFswiq3TGpNp1GMktqkR2Ns5AIQkATU=
-github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
-github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
-github.com/gofrs/flock v0.0.0-20190320160742-5135e617513b/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU=
-github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw=
+github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
+github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
-github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
-github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
-github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
-github.com/golang-jwt/jwt/v4 v4.0.1-0.20210824015611-2bd8ee77fc3a h1:PkXyoVlXSgfo6UaTddRi4S72pr8A75+F1+mSNBtumEo=
-github.com/golang-jwt/jwt/v4 v4.0.1-0.20210824015611-2bd8ee77fc3a/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
+github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA=
+github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
+github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
+github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
-github.com/golang/glog v1.0.0 h1:nfP3RFugxnNRyKgeWd4oI1nYvXpxrx8ck8ZrcizshdQ=
-github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4=
-github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
-github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
-github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
-github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e h1:1r7pUrabqp18hOBcwBwiTsbnFeTZHV9eER/QT5JVZxY=
-github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
-github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E=
-github.com/golang/mock v1.0.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
-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/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
-github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
-github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
-github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
-github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
-github.com/golang/mock v1.5.0 h1:jlYHihg//f7RRwuPfptm04yp4s7O6Kw8EZiVYIGcH0g=
-github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8=
-github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/glog v1.2.1 h1:OptwRhECazUx5ix5TTWC3EZhsZEHWcYWY4FQHTIubm4=
+github.com/golang/glog v1.2.1/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w=
+github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
+github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
-github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
-github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
-github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
-github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
-github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
-github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
-github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
-github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
-github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
-github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
-github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
-github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
-github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM=
-github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
-github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
-github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
-github.com/golang/snappy v0.0.2/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
-github.com/golang/snappy v0.0.3 h1:fHPg5GQYlCeLIPB9BZqMVR5nR9A+IM5zcgeTdjMYmLA=
+github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
+github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
-github.com/golangci/check v0.0.0-20180506172741-cfe4005ccda2/go.mod h1:k9Qvh+8juN+UKMCS/3jFtGICgW8O96FVaZsaxdzDkR4=
-github.com/golangci/dupl v0.0.0-20180902072040-3e9179ac440a/go.mod h1:ryS0uhF+x9jgbj/N71xsEqODy9BN81/GonCZiOzirOk=
-github.com/golangci/errcheck v0.0.0-20181223084120-ef45e06d44b6/go.mod h1:DbHgvLiFKX1Sh2T1w8Q/h4NAI8MHIpzCdnBUDTXU3I0=
-github.com/golangci/go-misc v0.0.0-20180628070357-927a3d87b613/go.mod h1:SyvUF2NxV+sN8upjjeVYr5W7tyxaT1JVtvhKhOn2ii8=
-github.com/golangci/go-tools v0.0.0-20190318055746-e32c54105b7c/go.mod h1:unzUULGw35sjyOYjUt0jMTXqHlZPpPc6e+xfO4cd6mM=
-github.com/golangci/goconst v0.0.0-20180610141641-041c5f2b40f3/go.mod h1:JXrF4TWy4tXYn62/9x8Wm/K/dm06p8tCKwFRDPZG/1o=
-github.com/golangci/gocyclo v0.0.0-20180528134321-2becd97e67ee/go.mod h1:ozx7R9SIwqmqf5pRP90DhR2Oay2UIjGuKheCBCNwAYU=
-github.com/golangci/gofmt v0.0.0-20181222123516-0b8337e80d98/go.mod h1:9qCChq59u/eW8im404Q2WWTrnBUQKjpNYKMbU4M7EFU=
-github.com/golangci/gofmt v0.0.0-20190930125516-244bba706f1a/go.mod h1:9qCChq59u/eW8im404Q2WWTrnBUQKjpNYKMbU4M7EFU=
-github.com/golangci/golangci-lint v1.17.2-0.20190910081718-bad04bb7378f/go.mod h1:kaqo8l0OZKYPtjNmG4z4HrWLgcYNIJ9B9q3LWri9uLg=
-github.com/golangci/golangci-lint v1.24.0/go.mod h1:yIqiAZ2SSQqg+1JeFlAdvEWjGVz4uu5jr4lrciqA1gE=
-github.com/golangci/gosec v0.0.0-20190211064107-66fb7fc33547/go.mod h1:0qUabqiIQgfmlAmulqxyiGkkyF6/tOGSnY2cnPVwrzU=
-github.com/golangci/ineffassign v0.0.0-20190609212857-42439a7714cc/go.mod h1:e5tpTHCfVze+7EpLEozzMB3eafxo2KT5veNg1k6byQU=
-github.com/golangci/lint-1 v0.0.0-20190420132249-ee948d087217/go.mod h1:66R6K6P6VWk9I95jvqGxkqJxVWGFy9XlDwLwVz1RCFg=
-github.com/golangci/lint-1 v0.0.0-20191013205115-297bf364a8e0/go.mod h1:66R6K6P6VWk9I95jvqGxkqJxVWGFy9XlDwLwVz1RCFg=
-github.com/golangci/maligned v0.0.0-20180506175553-b1d89398deca/go.mod h1:tvlJhZqDe4LMs4ZHD0oMUlt9G2LWuDGoisJTBzLMV9o=
-github.com/golangci/misspell v0.0.0-20180809174111-950f5d19e770/go.mod h1:dEbvlSfYbMQDtrpRMQU675gSDLDNa8sCPPChZ7PhiVA=
-github.com/golangci/prealloc v0.0.0-20180630174525-215b22d4de21/go.mod h1:tf5+bzsHdTM0bsB7+8mt0GUMvjCgwLpTapNZHU8AajI=
-github.com/golangci/revgrep v0.0.0-20180526074752-d9c87f5ffaf0/go.mod h1:qOQCunEYvmd/TLamH+7LlVccLvUH5kZNhbCgTHoBbp4=
-github.com/golangci/unconvert v0.0.0-20180507085042-28b1c447d1f4/go.mod h1:Izgrg8RkN3rCIMLGE9CyYmU9pY2Jer6DgANEnZ/L/cQ=
-github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
-github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
-github.com/google/cel-go v0.7.3 h1:8v9BSN0avuGwrHFKNCjfiQ/CE6+D6sW+BDyOVoEeP6o=
-github.com/google/cel-go v0.7.3/go.mod h1:4EtyFAHT5xNr0Msu0MJjyGxPUgdr9DlcaPyzLt/kkt8=
-github.com/google/cel-spec v0.5.0/go.mod h1:Nwjgxy5CbjlPrtCWjeDjUyKMl8w41YBYGjsyDdqk0xA=
-github.com/google/certificate-transparency-go v1.1.0/go.mod h1:i+Q7XY+ArBveOUT36jiHGfuSK1fHICIg6sUkRxPAbCs=
-github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
-github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
-github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
-github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
+github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
+github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
+github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU=
+github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
+github.com/google/cel-go v0.15.1 h1:iTgVZor2x9okXtmTrqO8cg4uvqIeaBcWhXtruaWFMYQ=
+github.com/google/cel-go v0.15.1/go.mod h1:YzWEoI07MC/a/wj9in8GeVatqfypkldgBlwXh9bCwqY=
+github.com/google/certificate-transparency-go v1.0.21/go.mod h1:QeJfpSbVSfYc7RgB3gJFj9cbuQMMchQxrWXz8Ruopmg=
+github.com/google/certificate-transparency-go v1.1.6 h1:SW5K3sr7ptST/pIvNkSVWMiJqemRmkjJPPT0jzXdOOY=
+github.com/google/certificate-transparency-go v1.1.6/go.mod h1:0OJjOsOk+wj6aYQgP7FU0ioQ0AJUmnWPFMqTjQeazPQ=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-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/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
-github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
-github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
-github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
-github.com/google/monologue v0.0.0-20190606152607-4b11a32b5934/go.mod h1:6NTfaQoUpg5QmPsCUWLR3ig33FHrKXhTtWzF0DVdmuk=
-github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
-github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
-github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
-github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
-github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
-github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
-github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
-github.com/google/pprof v0.0.0-20201009210932-67992a1a5a35/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
-github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
-github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
-github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
-github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
+github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
+github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/google/go-tpm v0.9.0 h1:sQF6YqWMi+SCXpsmS3fd21oPy/vSddwZry4JnmltHVk=
+github.com/google/go-tpm v0.9.0/go.mod h1:FkNVkc6C+IsvDI9Jw1OveJmxGZUUaKxtrpOS47QWKfU=
+github.com/google/go-tpm-tools v0.4.1 h1:gYU6iwRo0tY3V6NDnS6m+XYog+b3g6YFhHQl3sYaUL4=
+github.com/google/go-tpm-tools v0.4.1/go.mod h1:w03m0jynhTo7puXTYoyfpNOMqyQ9SB7sixnKWsS/1L0=
+github.com/google/go-tspi v0.3.0 h1:ADtq8RKfP+jrTyIWIZDIYcKOMecRqNJFOew2IT0Inus=
+github.com/google/go-tspi v0.3.0/go.mod h1:xfMGI3G0PhxCdNVcYr1C4C+EizojDg/TXuX5by8CiHI=
+github.com/google/pprof v0.0.0-20240402174815-29b9bb013b0f h1:f00RU+zOX+B3rLAmMMkzHUF2h1z4DeYR9tTCvEq2REY=
+github.com/google/pprof v0.0.0-20240402174815-29b9bb013b0f/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
-github.com/google/trillian v1.2.2-0.20190612132142-05461f4df60a/go.mod h1:YPmUVn5NGwgnDUgqlVyFGMTgaWlnSvH7W5p+NdOG8UA=
-github.com/google/trillian-examples v0.0.0-20190603134952-4e75ba15216c/go.mod h1:WgL3XZ3pA8/9cm7yxqWrZE6iZkESB2ItGxy5Fo6k2lk=
-github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o=
+github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
-github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
-github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
-github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
-github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
-github.com/googleapis/gax-go v2.0.0+incompatible h1:j0GKcs05QVmm7yesiZq2+9cxHkNK9YM6zKx4D2qucQU=
-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.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
-github.com/googleapis/gax-go/v2 v2.0.5 h1:sjZBwGj9Jlw33ImPtvFviGYvseOtDM7hkSKB7+Tv3SM=
-github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
-github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
-github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00 h1:l5lAOZEym3oK3SQ2HBHWsJUfbNBiTXJDeW2QDxw9AQ0=
-github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
-github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4=
-github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q=
-github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
-github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
-github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
-github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
-github.com/gostaticanalysis/analysisutil v0.0.0-20190318220348-4088753ea4d3/go.mod h1:eEOZF4jCKGi+aprrirO9e7WKB3beBRtWgqGunKl6pKE=
-github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
-github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
-github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
-github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw=
-github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
-github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
-github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q=
-github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
-github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
-github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
-github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
-github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
-github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
-github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=
-github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
-github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
-github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
-github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
-github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
-github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
-github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
-github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc=
+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.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs=
+github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0=
+github.com/googleapis/gax-go/v2 v2.12.0 h1:A+gCJKdRfqXkr+BIRGtZLibNXf0m1f9E4HG56etFpas=
+github.com/googleapis/gax-go/v2 v2.12.0/go.mod h1:y+aIqrI5eb1YGMVJfuV3185Ts/D7qKpsEkdD5+I6QGU=
+github.com/gorilla/context v0.0.0-20160226214623-1ea25387ff6f/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
+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.4.0/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
+github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
+github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
+github.com/groob/finalizer v0.0.0-20170707115354-4c2ed49aabda/go.mod h1:MyndkAZd5rUMdNogn35MWXBX1UiBigrU8eTj8DoAC2c=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.18.0 h1:RtRsiaGvWxcwd8y3BiRZxsylPT8hLWZ5SPcfI+3IDNk=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.18.0/go.mod h1:TzP6duP4Py2pHLVPPQp42aoYI92+PCrVotyR5e8Vqlk=
github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
-github.com/hashicorp/hcl v0.0.0-20180404174102-ef8a98b0bbce/go.mod h1:oZtUIOe8dh44I2q6ScRibXws4Ajl+d+nod3AaR9vL5w=
+github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c=
+github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
-github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
-github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
-github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
-github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
-github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
-github.com/huandu/xstrings v1.3.1 h1:4jgBlKK6tLKFvO8u5pmYjG91cqytmDCDvGh7ECVFfFs=
-github.com/huandu/xstrings v1.3.1/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
-github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
-github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
-github.com/icrowley/fake v0.0.0-20180203215853-4178557ae428/go.mod h1:uhpZMVGznybq1itEKXj6RYw9I71qK4kH+OGMjRC4KEo=
-github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
-github.com/imdario/mergo v0.3.11 h1:3tnifQM4i+fbajXKBHXWEH+KvNHqojZ778UH75j3bGA=
+github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
+github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
+github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
+github.com/huandu/xstrings v1.4.0 h1:D17IlohoQq4UcpqD7fDk80P7l+lwAmlFaBHgOipl2FU=
+github.com/huandu/xstrings v1.4.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
+github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4=
+github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
-github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU=
+github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
+github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
+github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo=
+github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
+github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8=
+github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
+github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA=
+github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE=
+github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s=
+github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o=
+github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY=
+github.com/jackc/pgconn v1.9.1-0.20210724152538-d89c8390a530/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI=
+github.com/jackc/pgconn v1.14.0/go.mod h1:9mBNlny0UvkgJdCDvdVHYSjI+8tD2rnKK69Wz8ti++E=
+github.com/jackc/pgconn v1.14.1 h1:smbxIaZA08n6YuxEX1sDyjV/qkbtUtkH20qLkR9MUR4=
+github.com/jackc/pgconn v1.14.1/go.mod h1:9mBNlny0UvkgJdCDvdVHYSjI+8tD2rnKK69Wz8ti++E=
+github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE=
+github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8=
+github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE=
+github.com/jackc/pgmock v0.0.0-20201204152224-4fe30f7445fd/go.mod h1:hrBW0Enj2AZTNpt/7Y5rr2xe/9Mn757Wtb2xeBzPv2c=
+github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65 h1:DadwsjnMwFjfWc9y5Wi/+Zz7xoE5ALHsRQlOctkOiHc=
+github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65/go.mod h1:5R2h2EEX+qri8jOWMbJCtaPWkrrNc7OHwsp2TCqp7ak=
+github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
+github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
+github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78=
+github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA=
+github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg=
+github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=
+github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=
+github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
+github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
+github.com/jackc/pgproto3/v2 v2.3.2 h1:7eY55bdBeCz1F2fTzSz69QC+pG46jYq9/jtSPiJ5nn0=
+github.com/jackc/pgproto3/v2 v2.3.2/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
+github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E=
+github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
+github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 h1:L0QtFUgDarD7Fpv9jeVMgy/+Ec0mtnmYuImjTz6dtDA=
+github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
+github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg=
+github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc=
+github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw=
+github.com/jackc/pgtype v1.8.1-0.20210724151600-32e20a603178/go.mod h1:C516IlIV9NKqfsMCXTdChteoXmwgUceqaLfjg2e3NlM=
+github.com/jackc/pgtype v1.14.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4=
+github.com/jackc/pgtype v1.14.1 h1:LyDar7M2K0tShCWqzJ/ctzF1QC3Wzc9c8a6cHE0PFdc=
+github.com/jackc/pgtype v1.14.1/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4=
+github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y=
+github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM=
+github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc=
+github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs=
+github.com/jackc/pgx/v4 v4.18.1 h1:YP7G1KABtKpB5IHrO9vYwSrCOhs7p3uqhvhhQBptya0=
+github.com/jackc/pgx/v4 v4.18.1/go.mod h1:FydWkUyadDmdNH/mHnGob881GawxeEm7TcMCzkb+qQE=
+github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
+github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
+github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
+github.com/jackc/puddle v1.3.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
-github.com/jingyugao/rowserrcheck v0.0.0-20191204022205-72ab7603b68a/go.mod h1:xRskid8CManxVta/ALEhJha/pweKBaVG6fWgc0yH25s=
-github.com/jirfag/go-printf-func-name v0.0.0-20191110105641-45db9963cdd3/go.mod h1:HEWGJkRDzjJY2sqdDwxccsGicWEf9BQOZsq2tV+xzM0=
-github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
-github.com/jmespath/go-jmespath v0.3.0 h1:OS12ieG61fsCg5+qLJ+SsW9NicxNkg3b25OyT2yCeUc=
-github.com/jmespath/go-jmespath v0.3.0/go.mod h1:9QtRXoHjLGCJ5IBSaohpXITPlowMeeYCZ7fLUTSywik=
-github.com/jmoiron/sqlx v1.2.1-0.20190826204134-d7d95172beb5/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks=
-github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
-github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
-github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
-github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
-github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
-github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
-github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
-github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
-github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
-github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a h1:FaWFmfWdAUKbSCtOU2QjDaorUexogfaMgbipgYATUMU=
-github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a/go.mod h1:UJSiEoRfvx3hP73CvoARgeLjaIOjybY9vj8PUPPFGeU=
-github.com/juju/ratelimit v1.0.1/go.mod h1:qapgC/Gy+xNh9UxzV13HGGl/6UXNN+ct+vwSgWNm/qk=
-github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
-github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
-github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
+github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
+github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
github.com/kevburnsjr/skipfilter v0.0.1 h1:EWl1lWUJfIehrKYIEkps0Cl67lCfS2pUM9iZFNajp7g=
github.com/kevburnsjr/skipfilter v0.0.1/go.mod h1:jfaRyFOYVUtIa6IIC+0mB1qiZqhHw+DKvFowCBuijSk=
-github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
-github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
-github.com/kisielk/gotool v0.0.0-20161130080628-0de1eaf82fa3/go.mod h1:jxZFDH7ILpTPQTk+E2s+z4CUas9lVNjIuKR4c5/zKgM=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
-github.com/klauspost/compress v1.4.0/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
-github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
-github.com/klauspost/compress v1.13.0 h1:2T7tUoQrQT+fQWdaY5rjWztFGAFwbGD04iPJg90ZiOs=
-github.com/klauspost/compress v1.13.0/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg=
-github.com/klauspost/cpuid v0.0.0-20180405133222-e7e905edc00e/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
-github.com/klauspost/cpuid v1.2.0 h1:NMpwD2G9JSFOE1/TJjGSo5zG7Yb2bTe7eq1jH+irmeE=
-github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
-github.com/klauspost/cpuid/v2 v2.0.6/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
-github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4=
-github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
+github.com/klauspost/compress v1.12.3/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg=
+github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4=
+github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
+github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c=
+github.com/klauspost/cpuid/v2 v2.2.6 h1:ndNyv040zDGIDh8thGkXYjnFtiN02M1PVVF+JE/48xc=
+github.com/klauspost/cpuid/v2 v2.2.6/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
-github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8=
-github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
-github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
-github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
-github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
+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/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
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/letsencrypt/pkcs11key v2.0.1-0.20170608213348-396559074696+incompatible/go.mod h1:iGYXKqDXt0cpBthCHdr9ZdsQwyGlYFh/+8xa4WzIQ34=
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
-github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
+github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
+github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
+github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
+github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/libdns/libdns v0.2.1 h1:Wu59T7wSHRgtA0cfxC+n1c/e+O3upJGWytknkmFEDis=
github.com/libdns/libdns v0.2.1/go.mod h1:yQCXzk1lEZmmCPa857bnk4TsOiqYasqpyOEeSObbb40=
-github.com/logrusorgru/aurora v0.0.0-20181002194514-a7b3b318ed4e/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4=
-github.com/lucas-clemente/quic-go v0.21.0 h1:ZdC8UBxUSBdPlEv1+4y4SqIBy54VA8bRxN7DmkQ0URs=
-github.com/lucas-clemente/quic-go v0.21.0/go.mod h1:BWkfkkOSJD1AxFNBqdjBZi6FznZ96bhdcvZiA+LDrY8=
-github.com/lunixbochs/vtclean v0.0.0-20180621232353-2d01aacdc34a/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI=
-github.com/lunixbochs/vtclean v1.0.0 h1:xu2sLAri4lGiovBDQKxl5mrXyESr3gUr5m5SM5+LVb8=
-github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI=
-github.com/magiconair/properties v1.7.6/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
-github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
-github.com/magiconair/properties v1.8.5 h1:b6kJs+EmPFMYGkow9GiUyCyOvIwYetYJ3fSaWak/Gls=
-github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
-github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
-github.com/manifoldco/promptui v0.8.0 h1:R95mMF+McvXZQ7j1g8ucVZE1gLP3Sv6j9vlF9kyRqQo=
-github.com/manifoldco/promptui v0.8.0/go.mod h1:n4zTdgP0vr0S3w7/O/g98U+e0gwLScEXGwov2nIKuGQ=
-github.com/marten-seemann/qpack v0.2.1 h1:jvTsT/HpCn2UZJdP+UUB53FfUUgeOyG5K1ns0OJOGVs=
-github.com/marten-seemann/qpack v0.2.1/go.mod h1:F7Gl5L1jIgN1D11ucXefiuJS9UMVP2opoCp2jDKb7wc=
-github.com/marten-seemann/qtls-go1-15 v0.1.4 h1:RehYMOyRW8hPVEja1KBVsFVNSm35Jj9Mvs5yNoZZ28A=
-github.com/marten-seemann/qtls-go1-15 v0.1.4/go.mod h1:GyFwywLKkRt+6mfU99csTEY1joMZz5vmB1WNZH3P81I=
-github.com/marten-seemann/qtls-go1-16 v0.1.3 h1:XEZ1xGorVy9u+lJq+WXNE+hiqRYLNvJGYmwfwKQN2gU=
-github.com/marten-seemann/qtls-go1-16 v0.1.3/go.mod h1:gNpI2Ol+lRS3WwSOtIUUtRwZEQMXjYK+dQSBFbethAk=
-github.com/marten-seemann/qtls-go1-17 v0.1.0-alpha.1 h1:LRFa3YRSlOAf9y56Szfhlh60CQrIMBSK/rneZD1gtuk=
-github.com/marten-seemann/qtls-go1-17 v0.1.0-alpha.1/go.mod h1:lQDiKZDfPagLmg1zMtEgoBMSTAORq6M08lBogD5FtBY=
-github.com/matoous/godox v0.0.0-20190911065817-5d6d842e92eb/go.mod h1:1BELzlh859Sh1c6+90blK8lbYy0kwQf1bYlBhBysy1s=
-github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
-github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
-github.com/mattn/go-colorable v0.1.6 h1:6Su7aK7lXmJ/U79bYtBjLNaha4Fs1Rg9plHpcH+vvnE=
+github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
+github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
+github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA=
+github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg=
+github.com/mastercactapus/proxyprotocol v0.0.4 h1:qSY75IZF30ZqIU9iW1ip3I7gTnm8wRAnGWqPxCBVgq0=
+github.com/mastercactapus/proxyprotocol v0.0.4/go.mod h1:X8FRVEDZz9FkrIoL4QYTBF4Ka4ELwTv0sah0/5NxCPw=
+github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
-github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
-github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
-github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
-github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
-github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
+github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
+github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
+github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
+github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
-github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
-github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
-github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
-github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw=
-github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
-github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
-github.com/mholt/acmez v0.1.3 h1:J7MmNIk4Qf9b8mAGqAh4XkNeowv3f1zW816yf4zt7Qk=
-github.com/mholt/acmez v0.1.3/go.mod h1:8qnn8QA/Ewx8E3ZSsmscqsIjhhpxuy9vqdgbX2ceceM=
-github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4=
-github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
-github.com/miekg/dns v1.1.42/go.mod h1:+evo5L0630/F6ca/Z9+GAqzhjGyn8/c+TBaOyfEl0V4=
-github.com/miekg/dns v1.1.43 h1:JKfpVSCB84vrAmHzyrsxB5NAr5kLoMXZArPSw7Qlgyg=
-github.com/miekg/dns v1.1.43/go.mod h1:+evo5L0630/F6ca/Z9+GAqzhjGyn8/c+TBaOyfEl0V4=
-github.com/miekg/pkcs11 v1.0.2/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs=
-github.com/miekg/pkcs11 v1.0.3-0.20190429190417-a667d056470f/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs=
-github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
-github.com/mitchellh/copystructure v1.0.0 h1:Laisrj+bAB6b/yJwB5Bt3ITZhGJdqmxquMKeZ+mmkFQ=
+github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
+github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
+github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI=
+github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
+github.com/mholt/acmez v1.2.0 h1:1hhLxSgY5FvH5HCnGUuwbKY2VQVo8IU7rxXKSnZ7F30=
+github.com/mholt/acmez v1.2.0/go.mod h1:VT9YwH1xgNX1kmYY89gY8xPJC84BFAisjo8Egigt4kE=
+github.com/micromdm/scep/v2 v2.1.0 h1:2fS9Rla7qRR266hvUoEauBJ7J6FhgssEiq2OkSKXmaU=
+github.com/micromdm/scep/v2 v2.1.0/go.mod h1:BkF7TkPPhmgJAMtHfP+sFTKXmgzNJgLQlvvGoOExBcc=
+github.com/miekg/dns v1.1.58 h1:ca2Hdkz+cDg/7eNF6V56jjzuZ4aCAE+DbVkILdQWG/4=
+github.com/miekg/dns v1.1.58/go.mod h1:Ypv+3b/KadlvW9vJfXOTf300O4UqaHFzFCuHz+rPkBY=
github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
-github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
+github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
+github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
-github.com/mitchellh/go-ps v0.0.0-20170309133038-4fdf99ab2936/go.mod h1:r1VsdOzOPt1ZSrGZWFoNhsAedKnEd6r9Np1+5blZCWk=
-github.com/mitchellh/go-ps v0.0.0-20190716172923-621e5597135b/go.mod h1:r1VsdOzOPt1ZSrGZWFoNhsAedKnEd6r9Np1+5blZCWk=
-github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
-github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
-github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
-github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
-github.com/mitchellh/mapstructure v0.0.0-20180220230111-00c29f56e238/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
+github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc=
+github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
-github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag=
-github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
-github.com/mitchellh/reflectwalk v1.0.0 h1:9D+8oIskB4VJBN5SFlmc27fSlIBZaov1Wpk/IfikLNY=
+github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
+github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
-github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
-github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
-github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
-github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
-github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
-github.com/mozilla/tls-observatory v0.0.0-20180409132520-8791a200eb40/go.mod h1:SrKMQvPiws7F7iqYp8/TX+IhxCYhzr6N/1yb8cwHsGk=
-github.com/mozilla/tls-observatory v0.0.0-20190404164649-a3c1b6cfecfd/go.mod h1:SrKMQvPiws7F7iqYp8/TX+IhxCYhzr6N/1yb8cwHsGk=
-github.com/mreiferson/go-httpclient v0.0.0-20160630210159-31f0106b4474/go.mod h1:OQA4XLvDbMgS8P0CevmM4m9Q3Jq4phKUzcocxuGJ5m8=
+github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
+github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/mschoch/smat v0.2.0 h1:8imxQsjDm8yFEAVBe7azKmKSgzSkZXDuKkSq9374khM=
github.com/mschoch/smat v0.2.0/go.mod h1:kc9mz7DoBKqDyiRL7VZN8KvXQMWeTaVnttLRXOlotKw=
-github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
-github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
-github.com/naoina/go-stringutil v0.1.0 h1:rCUeRUHjBjGTSHl0VC00jUPLz8/F9dDzYI70Hzifhks=
-github.com/naoina/go-stringutil v0.1.0/go.mod h1:XJ2SJL9jCtBh+P9q5btrd/Ylo8XwT/h1USek5+NqSA0=
-github.com/naoina/toml v0.1.1 h1:PT/lllxVVN0gzzSqSlHEmP8MJB4MY2U7STGxiouV4X8=
-github.com/naoina/toml v0.1.1/go.mod h1:NBIhNtsFMo3G2szEBne+bO4gS192HuIYRqfvOWb4i1E=
-github.com/nbutton23/zxcvbn-go v0.0.0-20160627004424-a22cb81b2ecd/go.mod h1:o96djdrsSGy3AWPyBgZMAGfxZNfgntdJG+11KU4QvbU=
-github.com/nbutton23/zxcvbn-go v0.0.0-20171102151520-eafdab6b0663/go.mod h1:o96djdrsSGy3AWPyBgZMAGfxZNfgntdJG+11KU4QvbU=
-github.com/nbutton23/zxcvbn-go v0.0.0-20180912185939-ae427f1e4c1d/go.mod h1:o96djdrsSGy3AWPyBgZMAGfxZNfgntdJG+11KU4QvbU=
-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/newrelic/go-agent v2.15.0+incompatible/go.mod h1:a8Fv1b/fYhFSReoTU6HDkTYIMZeSVNffmoS726Y0LzQ=
-github.com/ngdinhtoan/glide-cleanup v0.2.0/go.mod h1:UQzsmiDOb8YV3nOsCxK/c9zPpCZVNoHScRE3EO9pVMM=
-github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78=
-github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
-github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
-github.com/olekukonko/tablewriter v0.0.1/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
-github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
-github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
-github.com/onsi/ginkgo v1.11.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
-github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
-github.com/onsi/ginkgo v1.14.0 h1:2mOpI4JVVPBN+WQRa0WKH2eXR+Ey+uK4n7Zj0aYpIQA=
-github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY=
-github.com/onsi/ginkgo v1.15.0/go.mod h1:hF8qUzuuC8DJGygJH3726JnCZX4MYbRB8yFfISqnKUg=
-github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
-github.com/onsi/gomega v1.4.2/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
-github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
-github.com/onsi/gomega v1.8.1/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA=
-github.com/onsi/gomega v1.10.1 h1:o0+MgICZLuZ7xjH7Vx6zS/zcu93/BEp1VwkIW1mEXCE=
-github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
-github.com/onsi/gomega v1.10.5/go.mod h1:gza4q3jKQJijlu05nKWRCW/GavJumGt8aNRxWg7mt48=
-github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=
-github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8=
-github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
-github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=
-github.com/pelletier/go-toml v1.1.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
+github.com/onsi/ginkgo/v2 v2.17.1 h1:V++EzdbhI4ZV4ev0UTIj0PzhzOcReJFyJaLjtSF55M8=
+github.com/onsi/ginkgo/v2 v2.17.1/go.mod h1:llBI3WDLL9Z6taip6f33H76YcWtJv+7R3HigUjbIBOs=
+github.com/onsi/gomega v1.30.0 h1:hvMK7xYz4D3HapigLTeGdId/NcfQx1VHMJc60ew99+8=
+github.com/onsi/gomega v1.30.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
-github.com/pelletier/go-toml v1.9.3 h1:zeC5b1GviRUyKYd6OJPvBU/mcVDVoL1OhT17FCt5dSQ=
-github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
+github.com/pelletier/go-toml/v2 v2.2.0 h1:QLgLl2yMN7N+ruc31VynXs1vhMZa7CeHHejIeBAsoHo=
+github.com/pelletier/go-toml/v2 v2.2.0/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
+github.com/peterbourgon/diskv/v3 v3.0.1 h1:x06SQA46+PKIUftmEujdwSEpIx8kR+M9eLYsUxeYveU=
+github.com/peterbourgon/diskv/v3 v3.0.1/go.mod h1:kJ5Ny7vLdARGU3WUuy6uzO6T0nb/2gWcT1JiBvRmb5o=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
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/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI=
-github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
-github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
-github.com/pquerna/otp v1.0.0/go.mod h1:Zad1CMQfSQZI5KLpahDiSUX4tMMREnXw98IvL1nhgMk=
-github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
-github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
-github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
-github.com/prometheus/client_golang v0.9.4/go.mod h1:oCXIBxdI62A4cR6aTRJCgetEjecSIYzOEaeAn4iYEpM=
-github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
-github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
-github.com/prometheus/client_golang v1.10.1-0.20210603120351-253906201bda/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
-github.com/prometheus/client_golang v1.11.0 h1:HNkLOAEQMIDv/K+04rukrLx6ch7msSRwf3/SASFAGtQ=
-github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
-github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
-github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
-github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
-github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M=
-github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
-github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
-github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
-github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
-github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
-github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
-github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc=
-github.com/prometheus/common v0.30.0 h1:JEkYlQnpzrzQFxi6gnukFPdQ+ac82oRhzMcIduJu/Ug=
-github.com/prometheus/common v0.30.0/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls=
-github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
-github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
-github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
-github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
-github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
-github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
-github.com/prometheus/procfs v0.7.3 h1:4jVXhlkAyzOScmCkXBTOLRLTz8EeU+eyjrwB/EPq0VU=
-github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
-github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
-github.com/quasilyte/go-consistent v0.0.0-20190521200055-c6f3937de18c/go.mod h1:5STLWrekHfjyYwxBRVRXNOSewLJ3PWfDJd1VyTS21fI=
-github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
-github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
-github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
+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/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
+github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
+github.com/prometheus/client_golang v1.19.0 h1:ygXvpU1AoN1MhdzckN+PyD9QJOSD4x7kmXYlnfbA6JU=
+github.com/prometheus/client_golang v1.19.0/go.mod h1:ZRM9uEAypZakd+q/x7+gmsvXdURP+DABIEIjnmDdp+k=
+github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
+github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
+github.com/prometheus/common v0.52.2 h1:LW8Vk7BccEdONfrJBDffQGRtpSzi5CQaRZGtboOO2ck=
+github.com/prometheus/common v0.52.2/go.mod h1:lrWtQx+iDfn2mbH5GUzlH9TSHyfZpHkSiG1W7y3sF2Q=
+github.com/prometheus/procfs v0.13.0 h1:GqzLlQyfsPbaEHaQkO7tbDlriv/4o5Hudv6OXHGKX7o=
+github.com/prometheus/procfs v0.13.0/go.mod h1:cd4PFCR54QLnGKPaKGA6l+cfuNXtht43ZKY6tow0Y1g=
+github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo=
+github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A=
+github.com/quic-go/qtls-go1-20 v0.4.1 h1:D33340mCNDAIKBqXuAvexTNMUByrYmFYVfKfDN5nfFs=
+github.com/quic-go/qtls-go1-20 v0.4.1/go.mod h1:X9Nh97ZL80Z+bX/gUXMbipO6OxdiDi58b/fMC9mAL+k=
+github.com/quic-go/quic-go v0.40.1 h1:X3AGzUNFs0jVuO3esAGnTfvdgvL4fq655WaOi1snv1Q=
+github.com/quic-go/quic-go v0.40.1/go.mod h1:PeN7kuVJ4xZbxSv/4OX6S1USOX8MJvydwpTx31vx60c=
+github.com/redis/go-redis/v9 v9.5.1 h1:H1X4D3yHPaYrkL5X06Wh6xNVM/pX0Ft4RV0vMGvLBh8=
+github.com/redis/go-redis/v9 v9.5.1/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
-github.com/rs/cors v1.6.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU=
-github.com/rs/xid v1.2.1 h1:mhH9Nq+C1fY2l1XIpgxIiUOfNpRBYH1kKcr+qfKgjRc=
+github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
+github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
-github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo=
+github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc=
+github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
+github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU=
+github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc=
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
-github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
-github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
-github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
-github.com/ryanuber/go-glob v0.0.0-20170128012129-256dc444b735/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc=
-github.com/samfoo/ansi v0.0.0-20160124022901-b6bd2ded7189 h1:CmSpbxmewNQbzqztaY0bke1qzHhyNyC29wYgh17Gxfo=
-github.com/samfoo/ansi v0.0.0-20160124022901-b6bd2ded7189/go.mod h1:UUwuHEJ9zkkPDxspIHOa59PUeSkGFljESGzbxntLmIg=
-github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
-github.com/securego/gosec v0.0.0-20200103095621-79fbf3af8d83/go.mod h1:vvbZ2Ae7AzSq3/kywjUDxSNq2SJ27RxCz2un0H3ePqE=
-github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
-github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
-github.com/shirou/gopsutil v0.0.0-20180427012116-c95755e4bcd7/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
-github.com/shirou/gopsutil v0.0.0-20190901111213-e4ec7b275ada/go.mod h1:WWnYX4lzhCH5h/3YBfyVA3VbLYjlMZZAQcW9ojMexNc=
-github.com/shirou/w32 v0.0.0-20160930032740-bb4de0191aa4/go.mod h1:qsXQc7+bwAM3Q1u/4XEfrquwF8Lw7D7y5cD8CuHnfIc=
-github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ=
+github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
+github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
+github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ=
+github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
+github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
+github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
+github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
+github.com/schollz/jsonstore v1.1.0 h1:WZBDjgezFS34CHI+myb4s8GGpir3UMpy7vWoCeO0n6E=
+github.com/schollz/jsonstore v1.1.0/go.mod h1:15c6+9guw8vDRyozGjN3FoILt0wpruJk9Pi66vjaZfg=
+github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
-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/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8=
+github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
github.com/shurcooL/sanitized_anchor_name v1.0.0/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/sirupsen/logrus v1.0.5/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
-github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
-github.com/sirupsen/logrus v1.3.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
-github.com/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I=
-github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
-github.com/smallstep/assert v0.0.0-20180720014142-de77670473b5/go.mod h1:TC9A4+RjIOS+HyTH7wG17/gSqVv95uDw2J64dQZx7RE=
+github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
+github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
+github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
+github.com/slackhq/nebula v1.8.2 h1:9lpJlivzjBPWxs9Y2tQqmJ1cP6hq+3kIodw021t3LrQ=
+github.com/slackhq/nebula v1.8.2/go.mod h1:SVVwnlGdmLg387U0XQMOSHRrD3VlJeXqd2/x/w/vxPs=
github.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262 h1:unQFBIznI+VYD1/1fApl1A+9VcBk+9dcqGfnePY87LY=
github.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262/go.mod h1:MyOHs9Po2fbM1LHej6sBUT8ozbxmMOFG+E+rx/GSGuc=
-github.com/smallstep/certificates v0.15.14/go.mod h1:1L10hpdBHF2U88GVrA77qAJG7wjnacIr+sgBfIDdzDE=
-github.com/smallstep/certificates v0.15.15 h1:k9LUlLibqth02HZN4QDWDKMZDWu6oko4RgcTCpV8rOs=
-github.com/smallstep/certificates v0.15.15/go.mod h1:1L10hpdBHF2U88GVrA77qAJG7wjnacIr+sgBfIDdzDE=
-github.com/smallstep/certinfo v1.5.0/go.mod h1:UWVjfGJeUc8O9PDR6VqI2CgWrQbF1HNMld1BSarYV2Q=
-github.com/smallstep/cli v0.15.16 h1:K3YQxfK3GrcJTiikkNFhFTWVR8qPR2ftROfWDFGphqM=
-github.com/smallstep/cli v0.15.16/go.mod h1:HU1iYqmiz28BSjC38zX9zpcfI+3Yi0DTPuDGeeoWjeU=
-github.com/smallstep/nosql v0.3.6/go.mod h1:h1zC/Z54uNHc8euquLED4qJNCrMHd3nytA141ZZh4qQ=
-github.com/smallstep/nosql v0.3.7 h1:P5C1cCj89a/MbD+4k8k585fzoaZNISmRA06v3q6u5lU=
-github.com/smallstep/nosql v0.3.7/go.mod h1:mC+MOhUY1uV5S5vGmAwp1FSPfDB7iImiYn5nJCjzAdA=
-github.com/smallstep/truststore v0.9.6 h1:vNzEJmaJL0XOZD8uouXLmYu4/aP1UQ/wHUopH3qKeYA=
-github.com/smallstep/truststore v0.9.6/go.mod h1:HwHKRcBi0RUxxw1LYDpTRhYC4jZUuxPpkHdVonlkoDM=
-github.com/smallstep/zcrypto v0.0.0-20200203191936-fbc32cf76bce/go.mod h1:+F24VU3UCxfVFvvqgm5jNUFQOm/L6ed13ImwWGFgg/g=
-github.com/smallstep/zlint v0.0.0-20180727184541-d84eaafe274f/go.mod h1:GeHHT7sJDI9ti3oEaFnvx1F4N8n3ZSw2YM1+sbEoxc4=
-github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
-github.com/smartystreets/assertions v1.2.0 h1:42S6lae5dvLc7BrLu/0ugRtcFVjoJNMC/N3yZFZkDFs=
-github.com/smartystreets/assertions v1.2.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo=
-github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
-github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
-github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
-github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE=
-github.com/sourcegraph/go-diff v0.5.1/go.mod h1:j2dHj3m8aZgQO8lMTcTnBcXkRRRqi34cd2MNlA9u1mE=
-github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA=
+github.com/smallstep/certificates v0.25.0 h1:WWihtjQ7SprnRxDV44mBp8t5SMsNO5EWsQaEwy1rgFg=
+github.com/smallstep/certificates v0.25.0/go.mod h1:thJmekMKUplKYip+la99Lk4IwQej/oVH/zS9PVMagEE=
+github.com/smallstep/go-attestation v0.4.4-0.20240109183208-413678f90935 h1:kjYvkvS/Wdy0PVRDUAA0gGJIVSEZYhiAJtfwYgOYoGA=
+github.com/smallstep/go-attestation v0.4.4-0.20240109183208-413678f90935/go.mod h1:vNAduivU014fubg6ewygkAvQC0IQVXqdc8vaGl/0er4=
+github.com/smallstep/nosql v0.6.0 h1:ur7ysI8s9st0cMXnTvB8tA3+x5Eifmkb6hl4uqNV5jc=
+github.com/smallstep/nosql v0.6.0/go.mod h1:jOXwLtockXORUPPZ2MCUcIkGR6w0cN1QGZniY9DITQA=
+github.com/smallstep/truststore v0.12.1 h1:guLUKkc1UlsXeS3t6BuVMa4leOOpdiv02PCRTiy1WdY=
+github.com/smallstep/truststore v0.12.1/go.mod h1:M4mebeNy28KusGX3lJxpLARIktLcyqBOrj3ZiZ46pqw=
+github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
+github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
-github.com/spf13/afero v1.1.0/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
-github.com/spf13/afero v1.6.0 h1:xoax2sJ2DT8S8xA2paPFjDCScCNeWsg75VG0DLRreiY=
-github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I=
-github.com/spf13/cast v1.2.0/go.mod h1:r2rcYCSwa1IExKTDiTfzaxqT2FNHs8hODu4LnUfgKEg=
+github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
+github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
-github.com/spf13/cast v1.4.1 h1:s0hze+J0196ZfEMTs80N7UlFt0BDuQ7Q+JDnHiMWKdA=
-github.com/spf13/cast v1.4.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
-github.com/spf13/cobra v0.0.2/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
+github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
+github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
-github.com/spf13/cobra v1.1.1/go.mod h1:WnodtKOvamDL/PwE2M4iKs8aMDBZ5Q5klgD3qfVJQMI=
-github.com/spf13/jwalterweatherman v0.0.0-20180109140146-7c0cea34c8ec/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
+github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
+github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
-github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
-github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
-github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
-github.com/spf13/viper v1.0.2/go.mod h1:A8kyI5cUJhb8N+3pkfONlcEcZbueH6nhAm0Fq7SrnBM=
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
-github.com/spf13/viper v1.6.1/go.mod h1:t3iDnF5Jlj76alVNuyFBk5oUMCvsrkbvZK0WQdfDi5k=
-github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=
-github.com/spf13/viper v1.8.1 h1:Kq1fyeebqsBfbjZj4EL7gj2IO0mMaiyjYUWcUsl2O44=
-github.com/spf13/viper v1.8.1/go.mod h1:o0Pch8wJ9BVSWGQMbra6iw0oQ5oktSIBaujf1rJH9Ns=
-github.com/stoewer/go-strcase v1.2.0 h1:Z2iHWqGXH00XYgqDmNgQbIBxf3wrNq0F3feEy0ainaU=
-github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8=
+github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ=
+github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk=
+github.com/stoewer/go-strcase v1.3.0 h1:g0eASXYtp+yvN9fK8sH94oCIk0fau9uV1/ZdJ0AVEzs=
+github.com/stoewer/go-strcase v1.3.0/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
+github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
+github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
+github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
-github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
-github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
-github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
-github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA=
-github.com/thales-e-security/pool v0.0.2/go.mod h1:qtpMm2+thHtqhLzTwgDBj/OuNnMpupY8mv0Phz0gjhU=
-github.com/timakin/bodyclose v0.0.0-20190721030226-87058b9bfcec/go.mod h1:Qimiffbc6q9tBWlVV6x0P9sat/ao1xEkREYPPj9hphk=
-github.com/timakin/bodyclose v0.0.0-20190930140734-f7f2e9bca95e/go.mod h1:Qimiffbc6q9tBWlVV6x0P9sat/ao1xEkREYPPj9hphk=
-github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
-github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce/go.mod h1:o8v6yHRoik09Xen7gje4m9ERNah1d1PPsVq1VEx9vE4=
-github.com/tommy-muehle/go-mnd v1.3.1-0.20200224220436-e6f9a994e8fa/go.mod h1:dSUh0FtTP8VhvkL1S+gUR1OKd9ZnSaozuI6r3m6wOig=
-github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
+github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
+github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
+github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
+github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
+github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
+github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
+github.com/tailscale/tscert v0.0.0-20230806124524-28a91b69a046 h1:8rUlviSVOEe7TMk7W0gIPrW8MqEzYfZHpsNWSf8s2vg=
+github.com/tailscale/tscert v0.0.0-20230806124524-28a91b69a046/go.mod h1:kNGUQ3VESx3VZwRwA9MSCUegIl6+saPL8Noq82ozCaU=
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
-github.com/ultraware/funlen v0.0.1/go.mod h1:Dp4UiAus7Wdb9KUZsYWZEWiRzGuM2kXM1lPbfaF6xhA=
-github.com/ultraware/funlen v0.0.2/go.mod h1:Dp4UiAus7Wdb9KUZsYWZEWiRzGuM2kXM1lPbfaF6xhA=
-github.com/ultraware/whitespace v0.0.4/go.mod h1:aVMh/gQve5Maj9hQ/hg+F75lr/X5A89uZnzAmWSineA=
-github.com/unrolled/secure v1.0.9 h1:BWRuEb1vDrBFFDdbCnKkof3gZ35I/bnHGyt0LB0TNyQ=
-github.com/unrolled/secure v1.0.9/go.mod h1:fO+mEan+FLB0CdEnHf6Q4ZZVNqG+5fuLFnP8p0BXDPI=
-github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
-github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
-github.com/urfave/cli v1.22.4 h1:u7tSpNPPswAFymm8IehJhy4uJMlUuU/GmqSkvJ1InXA=
-github.com/urfave/cli v1.22.4/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
-github.com/urfave/negroni v1.0.0 h1:kIimOitoypq34K7TG7DUaJ9kq/N4Ofuwi1sjz0KipXc=
-github.com/urfave/negroni v1.0.0/go.mod h1:Meg73S6kFm/4PpbYdq35yYWoCZ9mS/YSx+lKnmiohz4=
-github.com/uudashr/gocognit v1.0.1/go.mod h1:j44Ayx2KW4+oB6SWMv8KsmHzZrOInQav7D3cQMJ5JUM=
-github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
-github.com/valyala/fasthttp v1.2.0/go.mod h1:4vX61m6KN+xDduDNwXrhIAVZaZaZiQ1luJk8LWSxF3s=
-github.com/valyala/quicktemplate v1.1.1/go.mod h1:EH+4AkTd43SvgIbQHYu59/cJyxDoOVRUAfrukLPuGJ4=
-github.com/valyala/quicktemplate v1.2.0/go.mod h1:EH+4AkTd43SvgIbQHYu59/cJyxDoOVRUAfrukLPuGJ4=
-github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio=
-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/weppos/publicsuffix-go v0.4.0/go.mod h1:z3LCPQ38eedDQSwmsSRW4Y7t2L8Ln16JPQ02lHAdn5k=
-github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
+github.com/unrolled/secure v1.14.0 h1:u9vJTU/pR4Bny0ntLUMxdfLtmIRGvQf2sEFuA0TG9AE=
+github.com/unrolled/secure v1.14.0/go.mod h1:BmF5hyM6tXczk3MpQkFf1hpKSRqCyhqcbiQtiAF7+40=
+github.com/urfave/cli v1.22.14 h1:ebbhrRiGK2i4naQJr+1Xj92HXZCrK7MsyTS/ob3HnAk=
+github.com/urfave/cli v1.22.14/go.mod h1:X0eDS6pD6Exaclxm99NJ3FiCDRED7vIHpx2mDOHLvkA=
+github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
+github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
-github.com/yosida95/uritemplate/v3 v3.0.1 h1:+Fs//CsT+x231WmUQhMHWMxZizMvpnkOVWop02mVCfs=
-github.com/yosida95/uritemplate/v3 v3.0.1/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
-github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
-github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
-github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
-github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
-github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
-github.com/yuin/goldmark v1.3.6/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
-github.com/yuin/goldmark v1.3.7 h1:NSaHgaeJFCtWXCBkBKXw0rhgMuJ0VoE9FB5mWldcrQ4=
-github.com/yuin/goldmark v1.3.7/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
-github.com/yuin/goldmark-highlighting v0.0.0-20210516132338-9216f9c5aa01 h1:0SJnXjE4jDClMW6grE0xpNhwpqbPwkBTn8zpVw5C0SI=
-github.com/yuin/goldmark-highlighting v0.0.0-20210516132338-9216f9c5aa01/go.mod h1:TwKQPa5XkCCRC2GRZ5wtfNUTQ2+9/i19mGRijFeJ4BE=
-github.com/zmap/rc2 v0.0.0-20131011165748-24b9757f5521/go.mod h1:3YZ9o3WnatTIZhuOtot4IcUfzoKVjUHqu6WALIyI0nE=
-github.com/zmap/rc2 v0.0.0-20190804163417-abaa70531248/go.mod h1:3YZ9o3WnatTIZhuOtot4IcUfzoKVjUHqu6WALIyI0nE=
-github.com/zmap/zcertificate v0.0.0-20180516150559-0e3d58b1bac4/go.mod h1:5iU54tB79AMBcySS0R2XIyZBAVmeHranShAFELYx7is=
-github.com/zmap/zcertificate v0.0.0-20190521191901-30e388164f71/go.mod h1:gIZi1KPgkZNUQzPZXsZrNnUnxy05nTc0+tmlqvIkhRw=
-github.com/zmap/zcrypto v0.0.0-20190329181646-dff83107394d/go.mod h1:ix3q2kpLy0ibAuFXlr7qOhPKwFRRSjuynGuTR8EUPCk=
-github.com/zmap/zlint v0.0.0-20190516161541-9047d02cf65a/go.mod h1:xwLbce0UzBXp44sIAL1cii+hoK8j4AxRKlymZA2AIcY=
-go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
-go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
-go.etcd.io/bbolt v1.3.6 h1:/ecaJf0sk1l4l6V4awd65v2C3ILy7MSj+s/x1ADCIMU=
-go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4=
-go.etcd.io/etcd v3.3.13+incompatible/go.mod h1:yaeTdrJi5lOmYerz05bd8+V7KubZs8YSFZfzsF9A6aI=
-go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs=
-go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g=
-go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ=
-go.mozilla.org/pkcs7 v0.0.0-20200128120323-432b2356ecb1/go.mod h1:SNgMg+EgDFwmvSmLRTNKC5fegJjB7v23qTQ0XLGUNHk=
-go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA=
-go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
-go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
-go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
-go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
-go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
-go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
-go.opencensus.io v0.23.0 h1:gqCw0LfLxScz8irSi8exQc7fyQ0fKQU/qnC/X8+V/1M=
-go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
-go.step.sm/cli-utils v0.2.0 h1:hpVu9+6dpv/7/Bd8nGJFc3V+gQ+TciSJRTu9TavDUQ4=
-go.step.sm/cli-utils v0.2.0/go.mod h1:+t4qCp5NO+080DdGkJxEh3xL5S4TcYC2JTPLMM72b6Y=
-go.step.sm/crypto v0.6.1/go.mod h1:AKS4yMZVZD4EGjpSkY4eibuMenrvKCscb+BpWMet8c0=
-go.step.sm/crypto v0.8.0 h1:S4qBPyy3hR7KWLybSkHB0H14pwFfYkom4RZ96JzmXig=
-go.step.sm/crypto v0.8.0/go.mod h1:+CYG05Mek1YDqi5WK0ERc6cOpKly2i/a5aZmU1sfGj0=
+github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
+github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
+github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
+github.com/yuin/goldmark v1.4.15/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
+github.com/yuin/goldmark v1.5.6 h1:COmQAWTCcGetChm3Ig7G/t8AFAN00t+o8Mt4cf7JpwA=
+github.com/yuin/goldmark v1.5.6/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
+github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc h1:+IAOyRda+RLrxa1WC7umKOZRsGq4QrFFMYApOeHzQwQ=
+github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc/go.mod h1:ovIvrum6DQJA4QsJSovrkC4saKHQVs7TvcaeO8AIl5I=
+github.com/zeebo/assert v1.1.0 h1:hU1L1vLTHsnO8x8c9KAR5GmM5QscxHg5RNU5z5qbUWY=
+github.com/zeebo/assert v1.1.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
+github.com/zeebo/blake3 v0.2.3 h1:TFoLXsjeXqRNFxSbk35Dk4YtszE/MQQGK10BH4ptoTg=
+github.com/zeebo/blake3 v0.2.3/go.mod h1:mjJjZpnsyIVtVgTOSpJ9vmRE4wgDeyt2HU3qXvvKCaQ=
+github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo=
+github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4=
+github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
+go.etcd.io/bbolt v1.3.9 h1:8x7aARPEXiXbHmtUwAIv7eV2fQFHrLLavdiJ3uzJXoI=
+go.etcd.io/bbolt v1.3.9/go.mod h1:zaO32+Ti0PK1ivdPtgMESzuzL2VPoIG1PCQNvOdo/dE=
+go.mozilla.org/pkcs7 v0.0.0-20210730143726-725912489c62/go.mod h1:SNgMg+EgDFwmvSmLRTNKC5fegJjB7v23qTQ0XLGUNHk=
+go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352 h1:CCriYyAfq1Br1aIYettdHZTy8mBTIPo7We18TuO/bak=
+go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352/go.mod h1:SNgMg+EgDFwmvSmLRTNKC5fegJjB7v23qTQ0XLGUNHk=
+go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
+go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 h1:aFJWCqJMNjENlcleuuOkGAPH82y0yULBScfXcIEdS24=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1/go.mod h1:sEGXWArGqc3tVa+ekntsN65DmVbVeW+7lTKTjZF3/Fo=
+go.opentelemetry.io/contrib/propagators/autoprop v0.42.0 h1:s2RzYOAqHVgG23q8fPWYChobUoZM6rJZ98EnylJr66w=
+go.opentelemetry.io/contrib/propagators/autoprop v0.42.0/go.mod h1:Mv/tWNtZn+NbALDb2XcItP0OM3lWWZjAfSroINxfW+Y=
+go.opentelemetry.io/contrib/propagators/aws v1.17.0 h1:IX8d7l2uRw61BlmZBOTQFaK+y22j6vytMVTs9wFrO+c=
+go.opentelemetry.io/contrib/propagators/aws v1.17.0/go.mod h1:pAlCYRWff4uGqRXOVn3WP8pDZ5E0K56bEoG7a1VSL4k=
+go.opentelemetry.io/contrib/propagators/b3 v1.17.0 h1:ImOVvHnku8jijXqkwCSyYKRDt2YrnGXD4BbhcpfbfJo=
+go.opentelemetry.io/contrib/propagators/b3 v1.17.0/go.mod h1:IkfUfMpKWmynvvE0264trz0sf32NRTZL4nuAN9AbWRc=
+go.opentelemetry.io/contrib/propagators/jaeger v1.17.0 h1:Zbpbmwav32Ea5jSotpmkWEl3a6Xvd4tw/3xxGO1i05Y=
+go.opentelemetry.io/contrib/propagators/jaeger v1.17.0/go.mod h1:tcTUAlmO8nuInPDSBVfG+CP6Mzjy5+gNV4mPxMbL0IA=
+go.opentelemetry.io/contrib/propagators/ot v1.17.0 h1:ufo2Vsz8l76eI47jFjuVyjyB3Ae2DmfiCV/o6Vc8ii0=
+go.opentelemetry.io/contrib/propagators/ot v1.17.0/go.mod h1:SbKPj5XGp8K/sGm05XblaIABgMgw2jDczP8gGeuaVLk=
+go.opentelemetry.io/otel v1.21.0 h1:hzLeKBZEL7Okw2mGzZ0cc4k/A7Fta0uoPgaJCr8fsFc=
+go.opentelemetry.io/otel v1.21.0/go.mod h1:QZzNPQPm1zLX4gZK4cMi+71eaorMSGT3A4znnUvNNEo=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0 h1:cl5P5/GIfFh4t6xyruOgJP5QiA1pw4fYYdv6nc6CBWw=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0/go.mod h1:zgBdWWAu7oEEMC06MMKc5NLbA/1YDXV1sMpSqEeLQLg=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.21.0 h1:tIqheXEFWAZ7O8A7m+J0aPTmpJN3YQ7qetUAdkkkKpk=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.21.0/go.mod h1:nUeKExfxAQVbiVFn32YXpXZZHZ61Cc3s3Rn1pDBGAb0=
+go.opentelemetry.io/otel/metric v1.21.0 h1:tlYWfeo+Bocx5kLEloTjbcDwBuELRrIFxwdQ36PlJu4=
+go.opentelemetry.io/otel/metric v1.21.0/go.mod h1:o1p3CA8nNHW8j5yuQLdc1eeqEaPfzug24uvsyIEJRWM=
+go.opentelemetry.io/otel/sdk v1.21.0 h1:FTt8qirL1EysG6sTQRZ5TokkU8d0ugCj8htOgThZXQ8=
+go.opentelemetry.io/otel/sdk v1.21.0/go.mod h1:Nna6Yv7PWTdgJHVRD9hIYywQBRx7pbox6nwBnZIxl/E=
+go.opentelemetry.io/otel/trace v1.21.0 h1:WD9i5gzvoUPuXIXH24ZNBudiarZDKuekPqi/E8fpfLc=
+go.opentelemetry.io/otel/trace v1.21.0/go.mod h1:LGbsEB0f9LGjN+OZaQQ26sohbOmiMR+BaslueVtS/qQ=
+go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I=
+go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM=
+go.step.sm/cli-utils v0.8.0 h1:b/Tc1/m3YuQq+u3ghTFP7Dz5zUekZj6GUmd5pCvkEXQ=
+go.step.sm/cli-utils v0.8.0/go.mod h1:S77aISrC0pKuflqiDfxxJlUbiXcAanyJ4POOnzFSxD4=
+go.step.sm/crypto v0.35.1 h1:QAZZ7Q8xaM4TdungGSAYw/zxpyH4fMYTkfaXVV9H7pY=
+go.step.sm/crypto v0.35.1/go.mod h1:vn8Vkx/Mbqgoe7AG8btC0qZ995Udm3e+JySuDS1LCJA=
+go.step.sm/linkedca v0.20.1 h1:bHDn1+UG1NgRrERkWbbCiAIvv4lD5NOFaswPDTyO5vU=
+go.step.sm/linkedca v0.20.1/go.mod h1:Vaq4+Umtjh7DLFI1KuIxeo598vfBzgSYZUjgVJ7Syxw=
+go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
+go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
-go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
-go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
-go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
-go.uber.org/goleak v1.1.10 h1:z+mqJhf6ss6BSfSM671tgKyZBFPTTJM+HLxnhPC3wu0=
-go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A=
+go.uber.org/automaxprocs v1.5.3 h1:kWazyxZUrS3Gs4qUpbwo5kEIMGe/DAvi5Z4tl2NW4j8=
+go.uber.org/automaxprocs v1.5.3/go.mod h1:eRbA25aqJrxAbsLO0xy5jVwPt7FQnRgjW+efnwa1WM0=
+go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
+go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
+go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU=
+go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
+go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4=
go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
-go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
-go.uber.org/multierr v1.7.0 h1:zaiO/rmgFjbmCXdSYJWQcdvOCsthmdaHfr3Gm2Kx4Ec=
-go.uber.org/multierr v1.7.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak=
+go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
+go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
+go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
-go.uber.org/zap v1.15.0/go.mod h1:Mb2vm2krFEG5DV0W9qcHBYFtp/Wku1cvYaqPsS/WYfc=
-go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo=
-go.uber.org/zap v1.19.0 h1:mZQZefskPPCMIBCSEH0v2/iUqqLrYtaeqwD6FUGUnFE=
-go.uber.org/zap v1.19.0/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI=
-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-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
-golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
-golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
+go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM=
+go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
+go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/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-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
-golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
-golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
-golang.org/x/crypto v0.0.0-20200414173820-0848c9571904/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
-golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
-golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
-golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
-golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 h1:HWj/xjIHfjYU5nVXpTM0s39J9CbLn7Cc5a7IC5rwsMQ=
-golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
-golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
-golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
-golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
-golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
-golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
-golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
-golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
-golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
-golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
-golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
-golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
-golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
-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/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
-golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
-golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
-golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
+golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
+golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
+golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
+golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
+golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
+golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30=
+golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
+golang.org/x/exp v0.0.0-20240404231335-c0f41cb1a7a0 h1:985EYyeCOxTpcgOTJpflJUwOeEz0CQOdPt73OzpE9F8=
+golang.org/x/exp v0.0.0-20240404231335-c0f41cb1a7a0/go.mod h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
-golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
-golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
-golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
-golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
-golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 h1:VLliZ0d+/avPrXXH+OakdXhpJuEoBZuwh1m2j7U6Iug=
-golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
-golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
-golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
-golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
-golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
-golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
-golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
-golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
-golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
-golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
-golang.org/x/net v0.0.0-20170915142106-8351a756f30f/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-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-20180911220305-26e67e76b6c3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/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-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20181220203305-927f97764cc3/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-20190301231341-16b79f2e4e95/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
+golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
+golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
+golang.org/x/net v0.0.0-20170726083632-f5079bd7f6f7/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
-golang.org/x/net v0.0.0-20190313220215-9f648a60d977/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
-golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
-golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
-golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
-golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
-golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
-golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
-golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
-golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
-golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
-golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
-golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
-golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
-golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
-golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
-golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
-golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
-golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
-golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
-golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
-golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/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-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=
-golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
-golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
-golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d h1:LO7XpTYMwTqxjLcGWPijK3vRXg1aWdlNOVOHRq45d7c=
-golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
-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.0.0-20190402181905-9f3314589c9a/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
-golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
-golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
-golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
-golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
-golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
-golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
-golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
-golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
-golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
-golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
-golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c h1:pkQiBZBvdos9qq4wBAHqlzuZHEXo07pqV06ef90u1WI=
-golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
-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/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
+golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
+golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
+golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w=
+golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
+golang.org/x/oauth2 v0.18.0 h1:09qnuIAgzdx1XplqJvW6CQqMCtGZykZWcXzPMPUusvI=
+golang.org/x/oauth2 v0.18.0/go.mod h1:Wf7knwG0MPoWIMMBgFlEaSUDaKskp0dCfrlJRJXbBi8=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
-golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sys v0.0.0-20171026204733-164713f0dfce/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
+golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+golang.org/x/sys v0.0.0-20170728174421-0f826bdd13b5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/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-20181026203630-95b1ffbd15a5/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-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20181128092732-4ed8d59d0b35/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/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-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190316082340-a2f829d7f35f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200413165638-669c56c373c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200828194041-157a740278f4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20201231184435-2d18734c6014/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf h1:2ucpDCmfkl8Bd/FsLtiD653Wf96cW37s+iGx93zsu4k=
-golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/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-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
+golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
-golang.org/x/term v0.0.0-20210503060354-a79de5458b56/go.mod h1:tfny5GFUkzUvx4ps4ajbZsCe5lw1metzhBm9T3x7oIY=
-golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b h1:9zKuko04nR4gjZ4+DNjHqRlAJqbJETHwiNKDqTfOjfE=
-golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
-golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
-golang.org/x/text v0.0.0-20170915090833-1cbadb444a80/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
+golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
+golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
+golang.org/x/term v0.19.0 h1:+ThwsDv+tYfnJFhF4L8jITxu1tdTWRTZpdsWgEgjL6Q=
+golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk=
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.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
-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.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
-golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
-golang.org/x/tools v0.0.0-20170915040203-e531a2a1c15f/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
-golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
-golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
-golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
+golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
+golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
+golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
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-20181117154741-2ddaf7f79a09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
-golang.org/x/tools v0.0.0-20190110163146-51295c7ec13a/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-20190121143147-24cd39ecf745/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
-golang.org/x/tools v0.0.0-20190221204921-83362c3779f5/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
-golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
-golang.org/x/tools v0.0.0-20190311215038-5c2858a9cfe5/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
-golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
-golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
-golang.org/x/tools v0.0.0-20190322203728-c1a832b0ad89/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
-golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
-golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
-golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
-golang.org/x/tools v0.0.0-20190521203540-521d6ed310dd/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
-golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
-golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
+golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
-golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
-golang.org/x/tools v0.0.0-20190719005602-e377ae9d6386/go.mod h1:jcCCGcm9btYwXyDqrUWc6MKQKKGJCWEQ3AfLSRIbEuI=
-golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20190909030654-5b82db07426d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20190910044552-dd2b5c81c578/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20191113232020-e2727e816f5a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20200102140908-9497f49d5709/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20200204192400-7124308813f3/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
-golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
-golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
-golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
-golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
-golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
-golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
-golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
-golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
-golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
-golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
-golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
-golang.org/x/tools v0.0.0-20201017001424-6003fad69a88/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU=
-golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
-golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
-golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
-golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
-golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
-golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
-golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
-golang.org/x/tools v0.1.2 h1:kRBLX7v7Af8W7Gdbbc908OJcdgtK8bOz9Uaj8/F1ACA=
-golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
+golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
+golang.org/x/tools v0.20.0 h1:hz/CVckiOxybQvFw6h7b/q80NTr9IUQb4s1IIzW7KNY=
+golang.org/x/tools v0.20.0/go.mod h1:WvitBU7JJf6A4jOdg4S1tviW9bhUxkgeCui/0JHctQg=
+golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-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.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
-google.golang.org/api v0.5.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
-google.golang.org/api v0.6.0/go.mod h1:btoxGiFvQNVUZQ8W08zLtrVS08CNpINPEfxXxgJL1Q4=
-google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
-google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
-google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
-google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
-google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
-google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
-google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
-google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
-google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
-google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
-google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
-google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
-google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
-google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
-google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
-google.golang.org/api v0.33.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=
-google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=
-google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE=
-google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=
-google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU=
-google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94=
-google.golang.org/api v0.44.0 h1:URs6qR1lAxDsqWITsQXI4ZkGiYJ5dHtRNiCpfs2OeKA=
-google.golang.org/api v0.44.0/go.mod h1:EBOGZqzyhtvMDoxwS97ctnh0zUmYY6CxqXsc1AvkYD8=
-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 v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
-google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
-google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
-google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
-google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
-google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
-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-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
-google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
-google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
-google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
-google.golang.org/genproto v0.0.0-20190605220351-eb0b1bdb6ae6/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s=
-google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
-google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
-google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
-google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
-google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
-google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
-google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
-google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
-google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
-google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
-google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
-google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
-google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
-google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
-google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
-google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
-google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
-google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
-google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
-google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
-google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
-google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
-google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20201019141844-1ed22bb0c154/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20201102152239-715cce707fb0/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A=
-google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c h1:wtujag7C+4D6KMoulW9YauvK2lgdvCMS260jsqqBXr0=
-google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
-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.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
-google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
-google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
-google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
-google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
-google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
-google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
-google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
-google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
-google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
-google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
-google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
-google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
-google.golang.org/grpc v1.32.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
-google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
-google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
-google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=
-google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
-google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
-google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
-google.golang.org/grpc v1.38.0 h1:/9BgsAsa5nWe26HqOlvlgJnqBuktYOLCgjCPqsa56W0=
-google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
-google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
-google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
-google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
-google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
-google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
-google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
-google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
-google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
-google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
-google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
+google.golang.org/api v0.153.0 h1:N1AwGhielyKFaUqH07/ZSIQR3uNPcV7NVw0vj+j4iR4=
+google.golang.org/api v0.153.0/go.mod h1:3qNJX5eOmhiWYc67jRA/3GsDw97UFb5ivv7Y2PrriAY=
+google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=
+google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds=
+google.golang.org/genproto v0.0.0-20240116215550-a9fa1716bcac h1:ZL/Teoy/ZGnzyrqK/Optxxp2pmVh+fmJ97slxSRyzUg=
+google.golang.org/genproto v0.0.0-20240116215550-a9fa1716bcac/go.mod h1:+Rvu7ElI+aLzyDQhpHMFMMltsD6m7nqpuWDd2CwJw3k=
+google.golang.org/genproto/googleapis/api v0.0.0-20240123012728-ef4313101c80 h1:Lj5rbfG876hIAYFjqiJnPHfhXbv+nzTWfm04Fg/XSVU=
+google.golang.org/genproto/googleapis/api v0.0.0-20240123012728-ef4313101c80/go.mod h1:4jWUdICTdgc3Ibxmr8nAJiiLHwQBY0UI0XZcEMaFKaA=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20240123012728-ef4313101c80 h1:AjyfHzEPEFp/NpvfN5g+KDla3EMojjhRVZc1i7cj+oM=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20240123012728-ef4313101c80/go.mod h1:PAREbraiVEVGVdTZsVWjSbbTtSyGbAgIIvni8a8CD5s=
+google.golang.org/grpc v1.61.0 h1:TOvOcuXn30kRao+gfcvsebNEa5iZIiLkisYEkf7R7o0=
+google.golang.org/grpc v1.61.0/go.mod h1:VUbo7IFqmF1QtCAstipjG0GIoq49KvMe9+h1jFLBNJs=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
-google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ=
-google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
-gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U=
-gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
+google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
+google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/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/cheggaaa/pb.v1 v1.0.28/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
-gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
-gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo=
-gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
-gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
-gopkg.in/ini.v1 v1.62.0 h1:duBzk771uxoUuOlyRLkHsygud9+5lrlGjdFBb4mSKDU=
-gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
-gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8=
-gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
-gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
-gopkg.in/square/go-jose.v2 v2.5.1 h1:7odma5RETjNHWJnR32wx8t+Io4djHE1PqxCFx3iiZ2w=
-gopkg.in/square/go-jose.v2 v2.5.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
-gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
-gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
-gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
-gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s=
+gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
+gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
+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/square/go-jose.v2 v2.6.0 h1:NGk74WTnPKBNUhNzQX7PYcTLUjoq7mzKk2OKbvwk2iI=
+gopkg.in/square/go-jose.v2 v2.6.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
+gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
-gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/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=
-honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
-honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
-honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
-honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
-howett.net/plist v0.0.0-20181124034731-591f970eefbb h1:jhnBjNi9UFpfpl8YZhA9CrOqpnJdvzuiHsl/dnxl11M=
-howett.net/plist v0.0.0-20181124034731-591f970eefbb/go.mod h1:vMygbs4qMhSZSc4lCUl2OEE+rDiIIJAIdR4m7MiMcm0=
-mvdan.cc/interfacer v0.0.0-20180901003855-c20040233aed/go.mod h1:Xkxe497xwlCKkIaQYRfC7CSLworTXY9RMqwhhCm+8Nc=
-mvdan.cc/lint v0.0.0-20170908181259-adc824a0674b/go.mod h1:2odslEg/xrtNQqCYg2/jCoyKnw3vv5biOc3JnIcYfL4=
-mvdan.cc/unparam v0.0.0-20190209190245-fbb59629db34/go.mod h1:H6SUd1XjIs+qQCyskXg5OFSrilMRUkD8ePJpHKDPaeY=
-mvdan.cc/unparam v0.0.0-20190720180237-d51796306d8f/go.mod h1:4G1h5nDURzA3bwVMZIVpwbkw+04kSxk3rAtzlimaUJw=
-rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
-rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
-rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
-software.sslmate.com/src/go-pkcs12 v0.0.0-20201103104416-57fc603b7f52/go.mod h1:/xvNRWUqm0+/ZMiF4EX00vrSCMsE4/NHb+Pt3freEeQ=
-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=
+howett.net/plist v1.0.1 h1:37GdZ8tP09Q35o9ych3ehygcsL+HqKSwzctveSlarvM=
+howett.net/plist v1.0.1/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g=
diff --git a/caddy/mercure/main.go b/caddy/mercure/main.go
index 72fa6eac..fba57edb 100644
--- a/caddy/mercure/main.go
+++ b/caddy/mercure/main.go
@@ -2,13 +2,23 @@
package main
import (
+ "github.com/caddyserver/caddy/v2"
caddycmd "github.com/caddyserver/caddy/v2/cmd"
+ "go.uber.org/automaxprocs/maxprocs"
+ "go.uber.org/zap"
+
// plug in Caddy modules here.
_ "github.com/caddyserver/caddy/v2/modules/standard"
_ "github.com/dunglas/mercure/caddy"
)
func main() {
+ undo, err := maxprocs.Set()
+ defer undo()
+ if err != nil {
+ caddy.Log().Warn("failed to set GOMAXPROCS", zap.Error(err))
+ }
+
caddycmd.Main()
}
diff --git a/charts/mercure/Chart.yaml b/charts/mercure/Chart.yaml
index 01c9c654..53134b4e 100644
--- a/charts/mercure/Chart.yaml
+++ b/charts/mercure/Chart.yaml
@@ -12,8 +12,8 @@ keywords:
- streaming-api
- async-api
type: application
-version: 0.13.0
-appVersion: "v0.13.0"
+version: 0.15.11
+appVersion: "v0.15.11"
maintainers:
- name: dunglas
email: kevin@dunglas.fr
diff --git a/charts/mercure/README.md b/charts/mercure/README.md
index 34a8599c..0eb411a7 100644
--- a/charts/mercure/README.md
+++ b/charts/mercure/README.md
@@ -1,6 +1,7 @@
+
# Mercure Chart for Kubernetes
-![Version: 0.13.0](https://img.shields.io/badge/Version-0.13.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: v0.13.0](https://img.shields.io/badge/AppVersion-v0.13.0-informational?style=flat-square)
+![Version: 0.15.11](https://img.shields.io/badge/Version-0.15.11-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: v0.15.11](https://img.shields.io/badge/AppVersion-v0.15.11-informational?style=flat-square)
A Helm chart to install a Mercure Hub in a Kubernetes cluster. Mercure is a protocol to push data updates to web browsers and other HTTP clients in a convenient, fast, reliable and battery-efficient way.
@@ -19,6 +20,8 @@ To install the chart with the release name `my-release`, run the following comma
|-----|------|---------|-------------|
| affinity | object | `{}` | [Affinity](https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/#affinity-and-anti-affinity) configuration. See the [API reference](https://kubernetes.io/docs/reference/kubernetes-api/workload-resources/pod-v1/#scheduling) for details. |
| autoscaling | object | Disabled by default. | Autoscaling must not be enabled unless you are using [the High Availability version](https://mercure.rocks/docs/hub/cluster) (see [values.yaml](values.yaml) for details). |
+| caddyExtraConfig | string | `""` | Inject snippet or named-routes options in the Caddyfile |
+| caddyExtraDirectives | string | `""` | Inject extra Caddy directives in the Caddyfile. |
| dev | bool | `false` | Enable the development mode, including the debug UI and the demo. |
| extraDirectives | string | `""` | Inject extra Mercure directives in the Caddyfile. |
| fullnameOverride | string | `""` | A name to substitute for the full names of resources. |
@@ -30,16 +33,14 @@ To install the chart with the release name `my-release`, run the following comma
| ingress.annotations | object | `{}` | Annotations to be added to the ingress. |
| ingress.className | string | `""` | Ingress [class name](https://kubernetes.io/docs/concepts/services-networking/ingress/#ingress-class). |
| ingress.enabled | bool | `false` | Enable [ingress](https://kubernetes.io/docs/concepts/services-networking/ingress/). |
-| ingress.hosts[0].host | string | `"mercure-example.local"` | |
-| ingress.hosts[0].paths[0].path | string | `"/"` | |
-| ingress.hosts[0].paths[0].pathType | string | `"ImplementationSpecific"` | |
+| ingress.hosts | list | See [values.yaml](values.yaml). | Ingress host configuration. |
| ingress.tls | list | See [values.yaml](values.yaml). | Ingress TLS configuration. |
| license | string | `""` | The license key for [the High Availability version](https://mercure.rocks/docs/hub/cluster) (not necessary is you use the FOSS version). |
| nameOverride | string | `""` | A name in place of the chart name for `app:` labels. |
| nodeSelector | object | `{}` | [Node selector](https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/#nodeselector) configuration. |
| persistence | object | `{"accessMode":"ReadWriteOnce","enabled":false,"existingClaim":"","size":"1Gi","storageClass":""}` | Enable persistence using [Persistent Volume Claims](http://kubernetes.io/docs/user-guide/persistent-volumes/), only useful if you the BoltDB transport. |
| persistence.accessMode | string | `"ReadWriteOnce"` | A manually managed Persistent Volume and Claim. Requires `persistence.enabled: true` If defined, PVC must be created manually before volume will be bound. |
-| persistence.existingClaim | string | `""` | a manually managed Persistent Volume Claim -- Requires `persistence.enabled: true` -- If defined, PVC must be created manually before volume will be bound |
+| persistence.existingClaim | string | `""` | If defined, PVC must be created manually before volume will be bound |
| persistence.storageClass | string | `""` | Mercure Data Persistent Volume Storage Class. If defined, `storageClassName: ` If set to `"-"``, `storageClassName: ""``, which disables dynamic provisioning. If undefined (the default) or set to `null`, no `storageClassName` spec is set, choosing the default provisioner. |
| podAnnotations | object | `{}` | Annotations to be added to pods. |
| podSecurityContext | object | `{}` | Pod [security context](https://kubernetes.io/docs/tasks/configure-pod-container/security-context/#set-the-security-context-for-a-pod). See the [API reference](https://kubernetes.io/docs/reference/kubernetes-api/workload-resources/pod-v1/#security-context) for details. |
@@ -50,6 +51,7 @@ To install the chart with the release name `my-release`, run the following comma
| securityContext | object | `{}` | Container [security context](https://kubernetes.io/docs/tasks/configure-pod-container/security-context/#set-the-security-context-for-a-container). See the [API reference](https://kubernetes.io/docs/reference/kubernetes-api/workload-resources/pod-v1/#security-context-1) for details. |
| service.annotations | object | `{}` | |
| service.port | int | `80` | Service port. |
+| service.targetPort | int | `80` | Service target port. |
| service.type | string | `"ClusterIP"` | Kubernetes [service type](https://kubernetes.io/docs/concepts/services-networking/service/#publishing-services-service-types). |
| serviceAccount.annotations | object | `{}` | Annotations to add to the service account. |
| serviceAccount.create | bool | `true` | Specifies whether a service account should be created. |
diff --git a/charts/mercure/README.md.gotmpl b/charts/mercure/README.md.gotmpl
index 607c0905..f40be758 100644
--- a/charts/mercure/README.md.gotmpl
+++ b/charts/mercure/README.md.gotmpl
@@ -1,3 +1,4 @@
+
# Mercure Chart for Kubernetes
{{ template "chart.badgesSection" . }}
diff --git a/charts/mercure/templates/deployment.yaml b/charts/mercure/templates/deployment.yaml
index 8c31c6b2..9e2348b6 100644
--- a/charts/mercure/templates/deployment.yaml
+++ b/charts/mercure/templates/deployment.yaml
@@ -13,10 +13,10 @@ spec:
{{- include "mercure.selectorLabels" . | nindent 6 }}
template:
metadata:
- {{- with .Values.podAnnotations }}
annotations:
checksum/configmap: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}
checksum/secret: {{ include (print $.Template.BasePath "/secrets.yaml") . | sha256sum }}
+ {{- with .Values.podAnnotations }}
{{- toYaml . | nindent 8 }}
{{- end }}
labels:
@@ -41,12 +41,22 @@ spec:
imagePullPolicy: {{ .Values.image.pullPolicy }}
env:
- name: SERVER_NAME
- value: :80
+ value: :{{ .Values.service.targetPort }}
- name: GLOBAL_OPTIONS
valueFrom:
configMapKeyRef:
name: {{ include "mercure.fullname" . }}
key: global-options
+ - name: CADDY_EXTRA_CONFIG
+ valueFrom:
+ secretKeyRef:
+ name: {{ include "mercure.fullname" . }}
+ key: caddy-extra-config
+ - name: CADDY_SERVER_EXTRA_DIRECTIVES
+ valueFrom:
+ secretKeyRef:
+ name: {{ include "mercure.fullname" . }}
+ key: caddy-extra-directives
- name: MERCURE_TRANSPORT_URL
valueFrom:
secretKeyRef:
@@ -89,7 +99,7 @@ spec:
{{- end }}
ports:
- name: http
- containerPort: 80
+ containerPort: {{ .Values.service.targetPort }}
protocol: TCP
livenessProbe:
httpGet:
@@ -113,6 +123,10 @@ spec:
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
+ {{- with .Values.topologySpreadConstraints }}
+ topologySpreadConstraints:
+ {{- tpl . $ | nindent 8 }}
+ {{- end }}
{{- with .Values.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
diff --git a/charts/mercure/templates/pdb.yaml b/charts/mercure/templates/pdb.yaml
new file mode 100644
index 00000000..d216f207
--- /dev/null
+++ b/charts/mercure/templates/pdb.yaml
@@ -0,0 +1,13 @@
+{{- if or (and .Values.autoscaling.enabled (gt (.Values.autoscaling.minReplicas | int) 1)) (and (not .Values.autoscaling.enabled) (gt (.Values.replicaCount | int) 1)) }}
+apiVersion: policy/v1
+kind: PodDisruptionBudget
+metadata:
+ name: {{ include "mercure.fullname" . }}
+ labels:
+ {{- include "mercure.labels" . | nindent 4 }}
+spec:
+ maxUnavailable: 1
+ selector:
+ matchLabels:
+ {{- include "mercure.selectorLabels" . | nindent 6 }}
+{{- end }}
diff --git a/charts/mercure/templates/pvc.yaml b/charts/mercure/templates/pvc.yaml
index 305ca5f8..e15eebba 100644
--- a/charts/mercure/templates/pvc.yaml
+++ b/charts/mercure/templates/pvc.yaml
@@ -11,7 +11,7 @@ spec:
resources:
requests:
storage: {{ .Values.persistence.size | quote }}
- {{- if .Values.persistence.storageClass -}}
- storageClassName: {{ if (eq "-" .Values.persistence.storageClass) -}}""{{ else }}{{ .Values.persistence.storageClass | quote }}{{ end -}}
+ {{- if .Values.persistence.storageClass }}
+ storageClassName: {{ if (eq "-" .Values.persistence.storageClass) -}}""{{ else }}{{ .Values.persistence.storageClass | quote }}{{ end -}}
{{- end -}}
{{- end }}
diff --git a/charts/mercure/templates/secrets.yaml b/charts/mercure/templates/secrets.yaml
index ae52fb52..a2b5f551 100644
--- a/charts/mercure/templates/secrets.yaml
+++ b/charts/mercure/templates/secrets.yaml
@@ -11,3 +11,5 @@ data:
subscriber-jwt-key: {{ .Values.subscriberJwtKey | default (randAlphaNum 40) | b64enc | quote }}
extra-directives: {{ .Values.extraDirectives | b64enc | quote }}
license: {{ .Values.license | b64enc | quote }}
+ caddy-extra-config: {{ .Values.caddyExtraConfig | b64enc | quote }}
+ caddy-extra-directives: {{ .Values.caddyExtraDirectives | b64enc | quote }}
diff --git a/charts/mercure/templates/service.yaml b/charts/mercure/templates/service.yaml
index 11779a9b..2490f8b3 100644
--- a/charts/mercure/templates/service.yaml
+++ b/charts/mercure/templates/service.yaml
@@ -13,7 +13,7 @@ spec:
type: {{ .Values.service.type }}
ports:
- port: {{ .Values.service.port }}
- targetPort: http
+ targetPort: {{ .Values.service.targetPort }}
protocol: TCP
name: http
selector:
diff --git a/charts/mercure/values.yaml b/charts/mercure/values.yaml
index 65cae098..ee7e3550 100644
--- a/charts/mercure/values.yaml
+++ b/charts/mercure/values.yaml
@@ -4,12 +4,16 @@
# -- Inject global options in the Caddyfile.
globalOptions: ""
+# -- Inject snippet or named-routes options in the Caddyfile
+caddyExtraConfig: ""
# -- Enable the development mode, including the debug UI and the demo.
dev: false
# -- The URL representation of the transport to use.
transportUrl: bolt:///data/mercure.db
# -- Inject extra Mercure directives in the Caddyfile.
extraDirectives: ""
+# -- Inject extra Caddy directives in the Caddyfile.
+caddyExtraDirectives: ""
# -- The JWT key to use for publishers, a random key will be generated if empty.
publisherJwtKey: ""
@@ -74,6 +78,8 @@ service:
type: ClusterIP
# -- Service port.
port: 80
+ # -- Service target port.
+ targetPort: 80
annotations: {}
ingress:
@@ -134,6 +140,16 @@ tolerations: []
# See the [API reference](https://kubernetes.io/docs/reference/kubernetes-api/workload-resources/pod-v1/#scheduling) for details.
affinity: {}
+# -- [Topology spread constraints](https://kubernetes.io/docs/concepts/scheduling-eviction/topology-spread-constraints/) configuration.
+# This is a template
+# topologySpreadConstraints: |
+# - maxSkew: 1
+# topologyKey: kubernetes.io/hostname
+# whenUnsatisfiable: DoNotSchedule
+# labelSelector:
+# matchLabels:
+# {{- include "mercure.selectorLabels" . | nindent 8 }}
+
# -- Enable persistence using [Persistent Volume Claims](http://kubernetes.io/docs/user-guide/persistent-volumes/), only useful if you the BoltDB transport.
persistence:
enabled: false
diff --git a/cmd/mercure/mercure.yaml b/cmd/mercure/mercure.yaml
index dcd184cc..2f4fbf6d 100644
--- a/cmd/mercure/mercure.yaml
+++ b/cmd/mercure/mercure.yaml
@@ -5,7 +5,7 @@ allow_anonymous: true
#key_file: fixtures/tls/server.key
cors_allowed_origins: [http://localhost:3000, http://localhost:3001, http://localhost:5000, http://localhost:8000]
transport_url: bolt://update.db
-jwt_key: '!ChangeMe!'
+jwt_key: '!ChangeThisMercureHubJWTSecretKey!'
publish_allowed_origins: [http://localhost:3000, http://localhost:3001, http://localhost:5000, http://localhost:8000]
subscriptions: true
diff --git a/cmd/root.go b/cmd/root.go
index cfbed422..d4a5bdca 100644
--- a/cmd/root.go
+++ b/cmd/root.go
@@ -19,7 +19,7 @@ other HTTP clients in a convenient, fast, reliable and battery-efficient way.
The Mercure Hub is the reference implementation of the Mercure protocol.
Go to https://mercure.rocks for more information!`,
- Run: func(cmd *cobra.Command, args []string) {
+ Run: func(_ *cobra.Command, _ []string) {
mercure.Start() //nolint:staticcheck
},
}
diff --git a/common/version.go b/common/version.go
index ebc1b0e3..640669ce 100644
--- a/common/version.go
+++ b/common/version.go
@@ -1,7 +1,6 @@
package common
import (
- "fmt"
"runtime"
"runtime/debug"
"strings"
@@ -31,24 +30,24 @@ func (v *AppVersionInfo) Shortline() string {
shortline := v.Version
if v.Commit != "" {
- shortline += fmt.Sprintf(", commit %s", v.Commit)
+ shortline += ", commit " + v.Commit
}
if v.BuildDate != "" {
- shortline += fmt.Sprintf(", built at %s", v.BuildDate)
+ shortline += ", built at " + v.BuildDate
}
return shortline
}
func (v *AppVersionInfo) ChangelogURL() string {
- path := "https://github.com/dunglas/mercure"
+ const path = "https://github.com/dunglas/mercure"
if v.Version == "dev" {
- return fmt.Sprintf("%s/releases/latest", path)
+ return path + "/releases/latest"
}
- return fmt.Sprintf("%s/releases/tag/v%s", path, strings.TrimPrefix(v.Version, "v"))
+ return path + "/releases/tag/v" + strings.TrimPrefix(v.Version, "v")
}
func (v *AppVersionInfo) NewMetricsCollector() *prometheus.GaugeVec {
@@ -81,7 +80,7 @@ func (v *AppVersionInfo) NewMetricsCollector() *prometheus.GaugeVec {
func init() { //nolint:gochecknoinits
if version == "dev" {
info, ok := debug.ReadBuildInfo()
- if ok && info.Main.Version != "(devel)" {
+ if ok && info.Main.Version != "(devel)" && info.Main.Version != "" {
version = info.Main.Version
}
}
diff --git a/common/version_test.go b/common/version_test.go
index 678eb1a3..6d352d10 100644
--- a/common/version_test.go
+++ b/common/version_test.go
@@ -17,8 +17,8 @@ func TestVersionInfo(t *testing.T) {
Architecture: "amd64",
}
- assert.Equal(t, v.Shortline(), "dev")
- assert.Equal(t, v.ChangelogURL(), "https://github.com/dunglas/mercure/releases/latest")
+ assert.Equal(t, "dev", v.Shortline())
+ assert.Equal(t, "https://github.com/dunglas/mercure/releases/latest", v.ChangelogURL())
}
func TestVersionInfoWithBuildDate(t *testing.T) {
@@ -31,8 +31,8 @@ func TestVersionInfoWithBuildDate(t *testing.T) {
Architecture: "amd64",
}
- assert.Equal(t, v.Shortline(), "1.0.0, built at 2020-05-03T18:42:44Z")
- assert.Equal(t, v.ChangelogURL(), "https://github.com/dunglas/mercure/releases/tag/v1.0.0")
+ assert.Equal(t, "1.0.0, built at 2020-05-03T18:42:44Z", v.Shortline())
+ assert.Equal(t, "https://github.com/dunglas/mercure/releases/tag/v1.0.0", v.ChangelogURL())
}
func TestVersionInfoWithCommit(t *testing.T) {
@@ -45,8 +45,8 @@ func TestVersionInfoWithCommit(t *testing.T) {
Architecture: "amd64",
}
- assert.Equal(t, v.Shortline(), "1.0.0, commit 96ee2b9")
- assert.Equal(t, v.ChangelogURL(), "https://github.com/dunglas/mercure/releases/tag/v1.0.0")
+ assert.Equal(t, "1.0.0, commit 96ee2b9", v.Shortline())
+ assert.Equal(t, "https://github.com/dunglas/mercure/releases/tag/v1.0.0", v.ChangelogURL())
}
func TestVersionInfoWithBuildDateAndCommit(t *testing.T) {
@@ -59,8 +59,8 @@ func TestVersionInfoWithBuildDateAndCommit(t *testing.T) {
Architecture: "amd64",
}
- assert.Equal(t, v.Shortline(), "1.0.0, commit 96ee2b9, built at 2020-05-03T18:42:44Z")
- assert.Equal(t, v.ChangelogURL(), "https://github.com/dunglas/mercure/releases/tag/v1.0.0")
+ assert.Equal(t, "1.0.0, commit 96ee2b9, built at 2020-05-03T18:42:44Z", v.Shortline())
+ assert.Equal(t, "https://github.com/dunglas/mercure/releases/tag/v1.0.0", v.ChangelogURL())
}
func TestVersionMetricsCollectorInitialization(t *testing.T) {
@@ -95,5 +95,5 @@ func TestVersionMetricsCollectorInitialization(t *testing.T) {
t.Fatal(err)
}
- assert.Equal(t, 1.0, *metricOut.Gauge.Value)
+ assert.Equal(t, 1.0, metricOut.GetGauge().GetValue()) //nolint:testifylint
}
diff --git a/config.go b/config.go
index 417579c3..c90a737f 100644
--- a/config.go
+++ b/config.go
@@ -30,6 +30,7 @@ func SetConfigDefaults(v *viper.Viper) {
v.SetDefault("acme_http01_addr", ":http")
v.SetDefault("heartbeat_interval", 40*time.Second) // Must be < 45s for compatibility with Yaffle/EventSource
v.SetDefault("read_timeout", 5*time.Second)
+ v.SetDefault("read_header_timeout", 3*time.Second)
v.SetDefault("write_timeout", 600*time.Second)
v.SetDefault("dispatch_timeout", 5*time.Second)
v.SetDefault("compress", false)
@@ -167,7 +168,7 @@ func NewHubFromViper(v *viper.Viper) (*Hub, error) { //nolint:funlen,gocognit
return nil, fmt.Errorf("invalid transport url: %w", err)
}
- t, err := NewTransport(u, logger, tss)
+ t, err := NewTransport(u, logger)
if err != nil {
return nil, err
}
diff --git a/config_test.go b/config_test.go
index c7386275..8313b82f 100644
--- a/config_test.go
+++ b/config_test.go
@@ -7,11 +7,12 @@ import (
"github.com/spf13/pflag"
"github.com/spf13/viper"
"github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
)
func TestMissingConfig(t *testing.T) {
err := ValidateConfig(viper.New())
- assert.EqualError(t, err, `invalid config: one of "jwt_key" or "publisher_jwt_key" configuration parameter must be defined`)
+ require.EqualError(t, err, `invalid config: one of "jwt_key" or "publisher_jwt_key" configuration parameter must be defined`)
}
func TestMissingKeyFile(t *testing.T) {
@@ -20,7 +21,7 @@ func TestMissingKeyFile(t *testing.T) {
v.Set("cert_file", "foo")
err := ValidateConfig(v)
- assert.EqualError(t, err, `invalid config: if the "cert_file" configuration parameter is defined, "key_file" must be defined too`)
+ require.EqualError(t, err, `invalid config: if the "cert_file" configuration parameter is defined, "key_file" must be defined too`)
}
func TestMissingCertFile(t *testing.T) {
@@ -29,7 +30,7 @@ func TestMissingCertFile(t *testing.T) {
v.Set("key_file", "foo")
err := ValidateConfig(v)
- assert.EqualError(t, err, `invalid config: if the "key_file" configuration parameter is defined, "cert_file" must be defined too`)
+ require.EqualError(t, err, `invalid config: if the "key_file" configuration parameter is defined, "cert_file" must be defined too`)
}
func TestSetFlags(t *testing.T) {
diff --git a/conformance-tests/.env b/conformance-tests/.env
new file mode 100644
index 00000000..e0be9337
--- /dev/null
+++ b/conformance-tests/.env
@@ -0,0 +1 @@
+CUSTOM_ID=1
diff --git a/conformance-tests/.gitignore b/conformance-tests/.gitignore
new file mode 100644
index 00000000..dbd64df8
--- /dev/null
+++ b/conformance-tests/.gitignore
@@ -0,0 +1,3 @@
+node_modules/
+test-results/
+playwright-report/
diff --git a/conformance-tests/mercure.spec.ts b/conformance-tests/mercure.spec.ts
new file mode 100644
index 00000000..d62b76d4
--- /dev/null
+++ b/conformance-tests/mercure.spec.ts
@@ -0,0 +1,125 @@
+import { test, expect } from '@playwright/test';
+import { randomBytes } from "crypto";
+
+function randomString() {
+ return randomBytes(20).toString('hex');
+}
+
+test.beforeEach(async ({ page }) => await page.goto('/'));
+
+test.describe('Publish update', () => {
+ const randomStrings: string[] = Array.from({ length: 6 }, randomString);
+
+ type Data = { name: string, updateTopics: string[], topicSelectors: string[], mustBeReceived: boolean, updateID?: string, private?: true };
+
+ const dataset: Data[] = [
+ { name: 'raw string', mustBeReceived: true, updateTopics: [randomStrings[0]], topicSelectors: [randomStrings[0]] },
+ { name: 'multiple topics', mustBeReceived: true, updateTopics: [randomString(), randomStrings[1]], topicSelectors: [randomStrings[1]] },
+ { name: 'multiple topic selectors', mustBeReceived: true, updateTopics: [randomStrings[2]], topicSelectors: ['foo', randomStrings[2]] },
+ { name: 'URI', mustBeReceived: true, updateTopics: [`https://example.net/foo/${randomStrings[3]}`], topicSelectors: [`https://example.net/foo/${randomStrings[3]}`] },
+ { name: 'URI template', mustBeReceived: true, updateTopics: [`https://example.net/foo/${randomStrings[4]}`], topicSelectors: ['https://example.net/foo/{random}'] },
+ { name: 'nonmatching raw string', mustBeReceived: false, updateTopics: [`will-not-match}`], topicSelectors: ['another-name'] },
+ { name: 'nonmatching URI', mustBeReceived: false, updateTopics: [`https://example.net/foo/will-not-match}`], topicSelectors: ['https://example.net/foo/another-name'] },
+ { name: 'nonmatching URI template', mustBeReceived: false, updateTopics: [`https://example.net/foo/will-not-match}`], topicSelectors: ['https://example.net/bar/{var}'] },
+ { name: 'private raw string', mustBeReceived: false, private: true, updateTopics: [randomStrings[0]], topicSelectors: [randomStrings[0]] },
+ { name: 'private URI', mustBeReceived: false, private: true, updateTopics: [`https://example.net/foo/${randomStrings[3]}`], topicSelectors: [`https://example.net/foo/${randomStrings[3]}`] },
+ { name: 'private URI template', mustBeReceived: false, private: true, updateTopics: [`https://example.net/foo/${randomStrings[4]}`], topicSelectors: ['https://example.net/foo/{random}'] },
+ ];
+
+ for (const data of dataset) {
+ test(data.name, async ({ page }) => {
+ page.on('console', msg => console.log(msg.text()));
+
+ data.updateID = `id-${JSON.stringify(data.updateTopics)}`;
+
+ const { received, contentType, status, body } = await page.evaluate(async (data) => {
+ const receivedResult = Symbol('received');
+ const notReceivedResult = Symbol('not received');
+
+ let resolveReady: () => void;
+ const ready = new Promise((resolve) => {
+ resolveReady = () => resolve(true);
+ });
+
+ let resolveReceived: () => void;
+ const received = new Promise((resolve) => {
+ resolveReceived = () => resolve(receivedResult);
+ });
+
+ const timeout = new Promise((resolve) => setTimeout(resolve, 2000, notReceivedResult));
+
+ const url = new window.URL('/.well-known/mercure', window.origin);
+ data.topicSelectors.forEach(topicSelector => url.searchParams.append('topic', topicSelector));
+
+ const event = new window.URLSearchParams();
+ data.updateTopics.forEach(updateTopic => event.append('topic', updateTopic));
+ event.set('id', data.updateID);
+ event.set('data', `data for
+
+ ${data.name}`);
+ if (data.private) event.set('private', 'on');
+
+ console.log(`data: ${JSON.stringify(data)}`);
+
+ console.log(`creating EventSource: ${url}`);
+ const es = new EventSource(url);
+ console.log('EventSource created');
+
+ es.onopen = () => {
+ console.log('EventSource opened');
+ resolveReady();
+ }
+
+ let id: string;
+ es.onmessage = (e) => {
+ console.log(`EventSource event received: ${e.data}`);
+ if (
+ e.type === 'message' &&
+ e.lastEventId === event.get('id') &&
+ e.data === event.get('data')
+ ) {
+ es.close();
+ resolveReceived();
+ }
+ };
+
+ await ready; // Wait for the EventSource to be ready
+
+ console.log(`Creating POST request: ${event.toString()}`);
+ const resp = await fetch(`/.well-known/mercure`, {
+ method: 'POST',
+ headers: { 'Authorization': 'Bearer eyJhbGciOiJIUzI1NiJ9.eyJtZXJjdXJlIjp7InB1Ymxpc2giOlsiKiJdLCJzdWJzY3JpYmUiOlsiKiJdfX0.bVXdlWXwfw9ySx7-iV5OpUSHo34RkjUdVzDLBcc6l_g' },
+ body: event,
+ });
+
+ id = await resp.text();
+ console.log(`POST request done: ${JSON.stringify({ status: resp.status, id, event: event.toString() })}`);
+
+ switch (await Promise.race([received, timeout])) {
+ case receivedResult:
+ return {
+ received: true,
+ contentType: resp.headers.get('Content-Type'),
+ status: resp.status,
+ body: id,
+ };
+
+ case notReceivedResult:
+ return {
+ received: false,
+ }
+ }
+
+ }, data);
+
+ expect(received).toBe(data.mustBeReceived);
+
+ if (data.mustBeReceived) {
+ expect(contentType).toMatch(/^text\/plain(?:$|;.*)/);
+ expect(status).toBe(200);
+ if (process.env.CUSTOM_ID)
+ expect(body).toBe(data.updateID);
+ }
+ });
+ }
+});
diff --git a/conformance-tests/package-lock.json b/conformance-tests/package-lock.json
new file mode 100644
index 00000000..d9460f29
--- /dev/null
+++ b/conformance-tests/package-lock.json
@@ -0,0 +1,75 @@
+{
+ "name": "conformance-tests",
+ "version": "1.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "conformance-tests",
+ "version": "1.0.0",
+ "license": "AGPL-3.0-or-later",
+ "dependencies": {
+ "dotenv": "^16.0.0"
+ },
+ "devDependencies": {
+ "@playwright/test": "^1.19.1"
+ }
+ },
+ "node_modules/@playwright/test": {
+ "version": "1.34.0",
+ "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.34.0.tgz",
+ "integrity": "sha512-GIALJVODOIrMflLV54H3Cow635OfrTwOu24ZTDyKC66uchtFX2NcCRq83cLdakMjZKYK78lODNLQSYBj2OgaTw==",
+ "dev": true,
+ "dependencies": {
+ "@types/node": "*",
+ "playwright-core": "1.34.0"
+ },
+ "bin": {
+ "playwright": "cli.js"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "optionalDependencies": {
+ "fsevents": "2.3.2"
+ }
+ },
+ "node_modules/@types/node": {
+ "version": "20.2.3",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.2.3.tgz",
+ "integrity": "sha512-pg9d0yC4rVNWQzX8U7xb4olIOFuuVL9za3bzMT2pu2SU0SNEi66i2qrvhE2qt0HvkhuCaWJu7pLNOt/Pj8BIrw==",
+ "dev": true
+ },
+ "node_modules/dotenv": {
+ "version": "16.0.3",
+ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.3.tgz",
+ "integrity": "sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
+ "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
+ "dev": true,
+ "hasInstallScript": true,
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/playwright-core": {
+ "version": "1.34.0",
+ "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.34.0.tgz",
+ "integrity": "sha512-fMUY1+iR6kYbJF/EsOOqzBA99ZHXbw9sYPNjwA4X/oV0hVF/1aGlWYBGPVUEqxBkGANDKMziYoOdKGU5DIP5Gg==",
+ "dev": true,
+ "engines": {
+ "node": ">=14"
+ }
+ }
+ }
+}
diff --git a/conformance-tests/package.json b/conformance-tests/package.json
new file mode 100644
index 00000000..a2cda71a
--- /dev/null
+++ b/conformance-tests/package.json
@@ -0,0 +1,21 @@
+{
+ "name": "conformance-tests",
+ "version": "1.0.0",
+ "description": "Conformance test suite of the Mercure protocol",
+ "main": "index.js",
+ "scripts": {},
+ "keywords": [
+ "mercure",
+ "sse",
+ "testing",
+ "spec"
+ ],
+ "author": "Kévin Dunglas",
+ "license": "AGPL-3.0-or-later",
+ "devDependencies": {
+ "@playwright/test": "^1.19.1"
+ },
+ "dependencies": {
+ "dotenv": "^16.0.0"
+ }
+}
diff --git a/conformance-tests/playwright.config.ts b/conformance-tests/playwright.config.ts
new file mode 100644
index 00000000..8a410c57
--- /dev/null
+++ b/conformance-tests/playwright.config.ts
@@ -0,0 +1,106 @@
+import type { PlaywrightTestConfig } from '@playwright/test';
+import { devices } from '@playwright/test';
+
+/**
+ * Read environment variables from file.
+ * https://github.com/motdotla/dotenv
+ */
+require('dotenv').config();
+
+/**
+ * See https://playwright.dev/docs/test-configuration.
+ */
+const config: PlaywrightTestConfig = {
+ testDir: './.',
+ /* Maximum time one test can run for. */
+ timeout: 10 * 1000,
+ expect: {
+ /**
+ * Maximum time expect() should wait for the condition to be met.
+ * For example in `await expect(locator).toHaveText();`
+ */
+ timeout: 5000
+ },
+ /* Fail the build on CI if you accidentally left test.only in the source code. */
+ forbidOnly: !!process.env.CI,
+ /* Retry on CI only */
+ retries: process.env.CI ? 2 : 0,
+ /* Opt out of parallel tests on CI. */
+ workers: process.env.CI ? 1 : undefined,
+ /* Reporter to use. See https://playwright.dev/docs/test-reporters */
+ reporter: 'html',
+ /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
+ use: {
+ /* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */
+ actionTimeout: 0,
+ /* Base URL to use in actions like `await page.goto('/')`. */
+ baseURL: process.env.BASE_URL ?? 'https://localhost/.well-known/mercure',
+ /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
+ trace: 'on-first-retry',
+ /* ignore HTTPS errors */
+ ignoreHTTPSErrors: true,
+ },
+
+ /* Configure projects for major browsers */
+ projects: [
+ {
+ name: 'chromium',
+ use: {
+ ...devices['Desktop Chrome'],
+ },
+ },
+
+ {
+ name: 'firefox',
+ use: {
+ ...devices['Desktop Firefox'],
+ },
+ },
+
+ {
+ name: 'webkit',
+ use: {
+ ...devices['Desktop Safari'],
+ },
+ },
+
+ /* Test against mobile viewports. */
+ // {
+ // name: 'Mobile Chrome',
+ // use: {
+ // ...devices['Pixel 5'],
+ // },
+ // },
+ // {
+ // name: 'Mobile Safari',
+ // use: {
+ // ...devices['iPhone 12'],
+ // },
+ // },
+
+ /* Test against branded browsers. */
+ // {
+ // name: 'Microsoft Edge',
+ // use: {
+ // channel: 'msedge',
+ // },
+ // },
+ // {
+ // name: 'Google Chrome',
+ // use: {
+ // channel: 'chrome',
+ // },
+ // },
+ ],
+
+ /* Folder for test artifacts such as screenshots, videos, traces, etc. */
+ // outputDir: 'test-results/',
+
+ /* Run your local dev server before starting the tests */
+ // webServer: {
+ // command: 'npm run start',
+ // port: 3000,
+ // },
+};
+
+export default config;
diff --git a/demo.go b/demo.go
index 269a5071..4789eb62 100644
--- a/demo.go
+++ b/demo.go
@@ -9,7 +9,10 @@ import (
"time"
)
+const linkSuffix = `>; rel="mercure"`
+
// uiContent is our static web server content.
+//
//go:embed public
var uiContent embed.FS
@@ -28,7 +31,7 @@ func (h *Hub) Demo(w http.ResponseWriter, r *http.Request) {
body := query.Get("body")
jwt := query.Get("jwt")
- hubLink := "<" + defaultHubURL + ">; rel=\"mercure\""
+ hubLink := "<" + defaultHubURL + linkSuffix
if h.cookieName != defaultCookieName {
hubLink = hubLink + "; cookie-name=\"" + h.cookieName + "\""
}
diff --git a/demo_test.go b/demo_test.go
index 81b786c1..4e4d6b1e 100644
--- a/demo_test.go
+++ b/demo_test.go
@@ -1,7 +1,8 @@
package mercure
import (
- "io/ioutil"
+ "io"
+ "net/http"
"net/http/httptest"
"testing"
"time"
@@ -10,7 +11,7 @@ import (
)
func TestEmptyBodyAndJWT(t *testing.T) {
- req := httptest.NewRequest("GET", "http://example.com/demo/foo.jsonld", nil)
+ req := httptest.NewRequest(http.MethodGet, "http://example.com/demo/foo.jsonld", nil)
w := httptest.NewRecorder()
h, _ := NewHub()
@@ -18,19 +19,20 @@ func TestEmptyBodyAndJWT(t *testing.T) {
resp := w.Result()
assert.Equal(t, "application/ld+json", resp.Header.Get("Content-Type"))
- assert.Equal(t, []string{"<" + defaultHubURL + ">; rel=\"mercure\"", "; rel=\"self\""}, resp.Header["Link"])
+ assert.Equal(t, []string{"<" + defaultHubURL + linkSuffix, "; rel=\"self\""}, resp.Header["Link"])
cookie := resp.Cookies()[0]
assert.Equal(t, "mercureAuthorization", cookie.Name)
assert.Empty(t, cookie.Value)
assert.True(t, cookie.Expires.Before(time.Now()))
- body, _ := ioutil.ReadAll(resp.Body)
+ defer resp.Body.Close()
+ body, _ := io.ReadAll(resp.Body)
assert.Equal(t, "", string(body))
}
func TestBodyAndJWT(t *testing.T) {
- req := httptest.NewRequest("GET", "http://example.com/demo/foo/bar.xml?body= &jwt=token", nil)
+ req := httptest.NewRequest(http.MethodGet, "http://example.com/demo/foo/bar.xml?body= &jwt=token", nil)
w := httptest.NewRecorder()
h, _ := NewHub()
@@ -38,13 +40,14 @@ func TestBodyAndJWT(t *testing.T) {
resp := w.Result()
assert.Contains(t, resp.Header.Get("Content-Type"), "xml") // Before Go 1.17, the charset wasn't set
- assert.Equal(t, []string{"<" + defaultHubURL + ">; rel=\"mercure\"", " &jwt=token>; rel=\"self\""}, resp.Header["Link"])
+ assert.Equal(t, []string{"<" + defaultHubURL + linkSuffix, " &jwt=token>; rel=\"self\""}, resp.Header["Link"])
cookie := resp.Cookies()[0]
assert.Equal(t, "mercureAuthorization", cookie.Name)
assert.Equal(t, "token", cookie.Value)
assert.Empty(t, cookie.Expires)
- body, _ := ioutil.ReadAll(resp.Body)
+ defer resp.Body.Close()
+ body, _ := io.ReadAll(resp.Body)
assert.Equal(t, " ", string(body))
}
diff --git a/docs/README.md b/docs/README.md
index 886ecfa3..9734c02a 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -5,20 +5,23 @@
## Protocol Specification
-* [The Specification](../spec/mercure.md) (also available as an [IETF's Internet Draft](https://datatracker.ietf.org/doc/draft-dunglas-mercure/) designed to be published as a RFC)
+* [The Specification](../spec/mercure.md) (also available as an [IETF's Internet Draft](https://datatracker.ietf.org/doc/draft-dunglas-mercure/) designed to be published as an RFC)
+* [Case studies and use cases](spec/use-cases.md)
* [Frequently Asked Questions](spec/faq.md)
-* [Use Cases](spec/use-cases.md)
* [OpenAPI spec](https://github.com/dunglas/mercure/blob/master/spec/openapi.yaml)
## Mercure.rocks Hub
* [Installing the Mercure.rocks Hub](hub/install.md)
* [Configuration](hub/config.md)
-* [Creating a cluster of Mercure.rocks Hubs](hub/cluster.md)
+* [The Cloud version](hub/cloud.md)
+* [Creating a cluster of hubs](hub/cluster.md)
* [Cookbooks](hub/cookbooks.md)
+* [Running behind NGINX](hub/nginx.md)
+* [Running behind Traefik Proxy](hub/traefik.md)
* [Troubleshooting](hub/troubleshooting.md)
* [Debug the Mercure.rocks Hub](hub/debug.md)
-* [Upgrade to newest versions](UPGRADE.md)
+* [Upgrade to new versions](UPGRADE.md)
* [Load Testing](hub/load-test.md)
## Ecosystem
@@ -27,3 +30,4 @@
* [Using a Mercure Service in Your GitHub Actions](ecosystem/github-actions.md)
* [Using Mercure and Hotwire to Stream Page Changes](ecosystem/hotwire.md)
* [Getting Help](ecosystem/help.md)
+* [Conformance Tests](ecosystem/conformance-tests.md)
diff --git a/docs/UPGRADE.md b/docs/UPGRADE.md
index 103317d5..0ea21531 100644
--- a/docs/UPGRADE.md
+++ b/docs/UPGRADE.md
@@ -1,8 +1,38 @@
# Upgrade
+## 0.14.4
+
+This release is built on top of [Caddy 2.6](https://github.com/caddyserver/caddy/releases/tag/v2.6.0).
+Caddy 2.6 removed support for single-hyphen long-form flags (such as `-config`), use the double-hyphen syntax instead (`--config`).
+
+## 0.14.3
+
+The `mercure_subscribers` field of the Prometheus endpoint has been renamed `mercure_subscribers_connected` for better interoperability (including with Datadog).
+
+## 0.14.1
+
+The default dev key changed from `!ChangeMe!` to `!ChangeThisMercureHubJWTSecretKey!` to respect the specification (they key must longer than 256 bits).
+
+## 0.14
+
+The query parameter allowing to fetch past events has been renamed `lastEventID`: in your clients, replace all occurences of the `Last-Event-ID` query parameter by `lastEventID`.
+
+Publishing public updates in topics not explictly listed in the `mercure.publish` JWT claim isn't supported anymore.
+To let your publishers publish (public and private updates) in all topics, use the special `*` topic selector:
+
+```patch
+ {
+ "mercure": {
+- "publish": []
++ "publish": ["*"]
+ }
+```
+
+Backward compatibility with the old version of the protocol (version 7) can be enabled by setting the `protocol_version_compatibility` directive to `7` in your `Caddyfile`.
+
## 0.13
-The `DEBUG` environment variable has gone. The the `GLOBAL_OPTIONS` environment variable to `debug` instead.
+The `DEBUG` environment variable has gone. Set the `GLOBAL_OPTIONS` environment variable to `debug` instead.
## 0.11
@@ -12,7 +42,7 @@ We still provide standalone binaries, but it's now a custom build of Caddy inclu
Builds of the legacy server are also available to ease the transition, but starting with version 0.12 only the Caddy-based builds will be provided (they have the `legacy` prefix).
-Relying on Caddy allows to use the Mercure.rocks Hub as a [reverse proxy](https://caddyserver.com/docs/quick-starts/reverse-proxy) for your website or API that also adds the Mercure well-known URL (`/.well-known/mercure`). Thanks to this new feature, the well-known URL can be on the same domain as your website or API, so you don't need to deal with [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS).
+Relying on Caddy allows to use the Mercure.rocks Hub as a [reverse proxy](https://caddyserver.com/docs/quick-starts/reverse-proxy) for your site or API that also adds the Mercure well-known URL (`/.well-known/mercure`). Thanks to this new feature, the well-known URL can be on the same domain as your site or API, so you don't need to deal with [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS).
All features provided by Caddy are also supported by this custom build: [HTTP/3 and h2c support](https://caddyserver.com/docs/json/apps/http/servers/#experimental_http3), [compression](https://caddyserver.com/docs/caddyfile/directives/encode), [Prometheus metrics](https://caddyserver.com/docs/metrics) (with additional Mercure-specific metrics), profiler (`/debug/pprof/`)...
diff --git a/docs/ecosystem/awesome.md b/docs/ecosystem/awesome.md
index 584c1366..0478ea79 100644
--- a/docs/ecosystem/awesome.md
+++ b/docs/ecosystem/awesome.md
@@ -60,18 +60,30 @@
## Learning Resources
-### English
+### English 🇺🇸
+* 📺 [API Updates in Real Time w. Mercure.rocks](https://www.youtube.com/watch?v=odNsxoHSkT4)
+* 📺 [Building async public APIs using HTTP/2+ and the Mercure protocol](https://www.youtube.com/watch?v=IUx47Tx0O8E)
+* 📺 [Real-time Notifications with Symfony and Mercure (Basics)](https://www.youtube.com/watch?v=kYNC47V7R_0)
+* 📺 [Real-time Chat App with Symfony and Mercure](https://www.youtube.com/watch?v=wnr2A4aKnPU)
* [Official Push and Real-Time Capabilities for Symfony and API Platform using Mercure (Symfony blog)](https://dunglas.fr/2019/03/official-push-and-real-time-capabilities-for-symfony-and-api-platform-mercure-protocol/)
* [Tech Workshop: Mercure by Kévin Dunglas at SensioLabs (SensioLabs)](https://blog.sensiolabs.com/2019/01/24/tech-workshop-mercure-kevin-dunglas-sensiolabs/)
* [Real-time messages with Mercure using Laravel](http://thedevopsguide.com/real-time-notifications-with-mercure/)
* [Using Mercure on Stackhero](https://www.stackhero.io/en/documentations/mercure-hub/getting-started)
+* [Mercure - install and run](https://mysiar.github.io/dev/2020/04/12/mercure-part1.html)
-### French
+### French 🇫🇷
-* [Tutoriel vidéo : Notifications instantanées avec Mercure (Grafikart)](https://www.grafikart.fr/tutoriels/symfony-mercure-1151)
-* [Mercure, un protocole pour pousser des mises à jour vers des navigateurs et app mobiles en temps réel (Les-Tilleuls.coop)](https://les-tilleuls.coop/fr/blog/article/mercure-un-protocole-pour-pousser-des-mises-a-jour-vers-des-navigateurs-et-app-mobiles-en-temps-reel)
+* 📺 [Notifications instantanées avec Mercure (Grafikart)](https://www.grafikart.fr/tutoriels/symfony-mercure-1151)
+* 📺 [Live Coding : Notifications temps réel avec Mercure](https://www.youtube.com/watch?v=tqqJ1ul2M-E)
+* 📺 [Explication des Server Sent Events (SSE) avec Mercure](https://www.youtube.com/watch?v=Q4LRN2wXuIc)
+* 📺 [Mercure : des UIs toujours synchronisées avec les données en BDD](https://www.youtube.com/watch?v=UcBa4AugNTE)
+* 📺 [Mercure, et PHP s'enamoure enfin du temps réel](https://www.youtube.com/watch?v=GugURP88Rgg)
+* 📺 [Async avec Messenger, AMQP et Mercure](https://www.youtube.com/watch?v=cHPbcuydJiA)
+* [Mercure, un protocole pour pousser des mises à jour vers des navigateurs et app mobiles en temps réel (Les-Tilleuls.coop)](https://les-tilleuls.coop/blog/mercure-un-protocole-pour-pousser-des-mises-a-jour-vers-des-navigateurs-et-app-mobiles-en-temps-reel)
+* [Symfony et Mercure](https://afsy.fr/avent/2019/21-symfony-et-mercure)
+* [À la découverte de Mercure](https://blog.eleven-labs.com/fr/a-la-decouverte-de-mercure/)
-### German
+### German 🇩🇪
* [Neue Symfony-Komponente: Mercure ermöglicht Echtzeitübertragung](https://entwickler.de/online/php/symfony-mercure-komponente-579885243.html)
diff --git a/docs/ecosystem/conformance-tests.md b/docs/ecosystem/conformance-tests.md
new file mode 100644
index 00000000..08ce25e8
--- /dev/null
+++ b/docs/ecosystem/conformance-tests.md
@@ -0,0 +1,23 @@
+# Conformance Tests
+
+We provide conformance tests to check if a hub is compliant with the Mercure specification.
+This test suite is based on [Playwright](https://playwright.dev/).
+
+## Install
+
+1. Clone the repository: `git clone https://github.com/dunglas/mercure`
+2. Go in the conformance tests directory: `cd conformance-tests`
+3. Install the dependencies: `npm ci`
+4. Install Playwright: `npx playwright install --with-deps`
+5. Run the test suite: `npx playwright test`
+
+## Configuration
+
+The test suite can be configured by setting environment variables:
+
+* `BASE_URL`: the URL of the hub to test
+* `CUSTOM_ID`: enable or disable tests related to custom IDs support
+
+## See Also
+
+* [The load test](../hub/load-test.md)
diff --git a/docs/ecosystem/github-actions.md b/docs/ecosystem/github-actions.md
index 93d730fd..e76c65f6 100644
--- a/docs/ecosystem/github-actions.md
+++ b/docs/ecosystem/github-actions.md
@@ -17,8 +17,8 @@ jobs:
image: dunglas/mercure
env:
SERVER_NAME: :1337
- MERCURE_PUBLISHER_JWT_KEY: '!ChangeMe!'
- MERCURE_SUBSCRIBER_JWT_KEY: '!ChangeMe!'
+ MERCURE_PUBLISHER_JWT_KEY: '!ChangeThisMercureHubJWTSecretKey!'
+ MERCURE_SUBSCRIBER_JWT_KEY: '!ChangeThisMercureHubJWTSecretKey!'
MERCURE_EXTRA_DIRECTIVES: |
# Custom directives, see https://mercure.rocks/docs/hub/config
anonymous
diff --git a/docs/ecosystem/help.md b/docs/ecosystem/help.md
index 40589105..5a20bee3 100644
--- a/docs/ecosystem/help.md
+++ b/docs/ecosystem/help.md
@@ -2,11 +2,11 @@
## Managed and On-Premise Support
-* For support requests related to the [managed](https://mercure.rocks/pricing) or the [on premise](https://mercure.rocks/docs/hub/cluster#purchasing) version of Mercure.rocks, send a mail to [contact@mercure.rocks](mailto:contact@mercure.rocks).
+For support requests related to the [Cloud](../hub/cloud.md) or the [On Premise](../hub/cluster.md#purchasing) version of Mercure.rocks, send a mail to [contact@mercure.rocks](mailto:contact@mercure.rocks?subject=Support%20request).
## Community Support
-* [StackOverflow: ask question on using the `mercure` tag](https://stackoverflow.com/questions/tagged/mercure)
+* [StackOverflow: ask questions using the `mercure` tag](https://stackoverflow.com/questions/tagged/mercure)
* [Slack: chat with the community on the `#mercure` channel on Symfony's Slack](https://symfony.com/slack)
## Commercial Support
@@ -17,4 +17,4 @@
## Training
-[Masterclass](https://masterclass.les-tilleuls.coop) organizes [official training sessions about the Mercure protocol](https://masterclass.les-tilleuls.coop/en/trainings/discover-mercure) (available in English and in French).
+Les-Tilleuls.coop also organizes [official training sessions about the Mercure protocol](https://les-tilleuls.coop/en/masterclass/trainings/introduction-to-mercure) (available in English and in French).
diff --git a/docs/ecosystem/hotwire.md b/docs/ecosystem/hotwire.md
index a2c4958c..062f2229 100644
--- a/docs/ecosystem/hotwire.md
+++ b/docs/ecosystem/hotwire.md
@@ -21,12 +21,14 @@ The native [`EventSource` class](https://developer.mozilla.org/en-US/docs/Web/AP
To broadcast messages through Turbo Streams, simply send a `POST` HTTP request to [the Mercure hub](../hub/install.md):
- curl \
- -H 'Authorization: Bearer ' \
- -d 'topic=my-stream' \
- -d 'data=' \
+ -d 'topic=my-stream' \
+ -d 'data= console.log(e); // do something with the payload
```
-The `EventSource` class is available [in all modern web browsers](https://caniuse.com/#feat=eventsource). And for legacy browsers, [there are polyfills](ecosystem/awesome.md#useful-related-libraries).
+The `EventSource` class is available [in all modern web browsers](https://caniuse.com/eventsource). And for legacy browsers, [there are polyfills](ecosystem/awesome.md#useful-related-libraries).
## Closing Connection
It is important to close this connection between the client and the hub if it is no longer needed.
-Opened connections have a continuous buffer that will drain your application resources.
+Opened connections have a continuous buffer that will drain your application resources.
This is especially true when using Single Page Applications (based on e.g. ReactJS): the connection is maintained even if the component that created it is unmounted.
To close the connection, call `eventSource.close()`.
@@ -66,14 +66,16 @@ To dispatch an update, the publisher (an application server, a web browser...) n
```http
POST example.com HTTP/1.1
-Bearer eyJhbGciOiJIUzI1NiJ9.eyJtZXJjdXJlIjp7InB1Ymxpc2giOlsiKiJdLCJzdWJzY3JpYmUiOlsiaHR0cHM6Ly9leGFtcGxlLmNvbS9teS1wcml2YXRlLXRvcGljIiwie3NjaGVtZX06Ly97K2hvc3R9L2RlbW8vYm9va3Mve2lkfS5qc29ubGQiLCIvLndlbGwta25vd24vbWVyY3VyZS9zdWJzY3JpcHRpb25zey90b3BpY317L3N1YnNjcmliZXJ9Il0sInBheWxvYWQiOnsidXNlciI6Imh0dHBzOi8vZXhhbXBsZS5jb20vdXNlcnMvZHVuZ2xhcyIsInJlbW90ZUFkZHIiOiIxMjcuMC4wLjEifX19.z5YrkHwtkz3O_nOnhC_FP7_bmeISe3eykAkGbAl5K7c
+Bearer eyJhbGciOiJIUzI1NiJ9.eyJtZXJjdXJlIjp7InB1Ymxpc2giOlsiKiJdLCJzdWJzY3JpYmUiOlsiaHR0cHM6Ly9leGFtcGxlLmNvbS9teS1wcml2YXRlLXRvcGljIiwie3NjaGVtZX06Ly97K2hvc3R9L2RlbW8vYm9va3Mve2lkfS5qc29ubGQiLCIvLndlbGwta25vd24vbWVyY3VyZS9zdWJzY3JpcHRpb25zey90b3BpY317L3N1YnNjcmliZXJ9Il0sInBheWxvYWQiOnsidXNlciI6Imh0dHBzOi8vZXhhbXBsZS5jb20vdXNlcnMvZHVuZ2xhcyIsInJlbW90ZUFkZHIiOiIxMjcuMC4wLjEifX19.KKPIikwUzRuB3DTpVw6ajzwSChwFw5omBMmMcWKiDcM
topic=https://example.com/books/1&data={"foo": "updated value"}
```
Example using [curl](https://curl.haxx.se/):
- curl -d 'topic=https://example.com/books/1' -d 'data={"foo": "updated value"}' -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJtZXJjdXJlIjp7InB1Ymxpc2giOlsiKiJdLCJzdWJzY3JpYmUiOlsiaHR0cHM6Ly9leGFtcGxlLmNvbS9teS1wcml2YXRlLXRvcGljIiwie3NjaGVtZX06Ly97K2hvc3R9L2RlbW8vYm9va3Mve2lkfS5qc29ubGQiLCIvLndlbGwta25vd24vbWVyY3VyZS9zdWJzY3JpcHRpb25zey90b3BpY317L3N1YnNjcmliZXJ9Il0sInBheWxvYWQiOnsidXNlciI6Imh0dHBzOi8vZXhhbXBsZS5jb20vdXNlcnMvZHVuZ2xhcyIsInJlbW90ZUFkZHIiOiIxMjcuMC4wLjEifX19.z5YrkHwtkz3O_nOnhC_FP7_bmeISe3eykAkGbAl5K7c' -X POST https://localhost/.well-known/mercure
+```bash
+curl -d 'topic=https://example.com/books/1' -d 'data={"foo": "updated value"}' -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJtZXJjdXJlIjp7InB1Ymxpc2giOlsiKiJdLCJzdWJzY3JpYmUiOlsiaHR0cHM6Ly9leGFtcGxlLmNvbS9teS1wcml2YXRlLXRvcGljIiwie3NjaGVtZX06Ly97K2hvc3R9L2RlbW8vYm9va3Mve2lkfS5qc29ubGQiLCIvLndlbGwta25vd24vbWVyY3VyZS9zdWJzY3JpcHRpb25zey90b3BpY317L3N1YnNjcmliZXJ9Il0sInBheWxvYWQiOnsidXNlciI6Imh0dHBzOi8vZXhhbXBsZS5jb20vdXNlcnMvZHVuZ2xhcyIsInJlbW90ZUFkZHIiOiIxMjcuMC4wLjEifX19.KKPIikwUzRuB3DTpVw6ajzwSChwFw5omBMmMcWKiDcM' -X POST https://localhost/.well-known/mercure
+```
Example using [Node.js](https://nodejs.org/) / [Serverless](https://serverless.com/):
@@ -94,7 +96,7 @@ const req = http.request({
path: '/.well-known/mercure',
method: 'POST',
headers: {
- Authorization: 'Bearer eyJhbGciOiJIUzI1NiJ9.eyJtZXJjdXJlIjp7InB1Ymxpc2giOlsiKiJdLCJzdWJzY3JpYmUiOlsiaHR0cHM6Ly9leGFtcGxlLmNvbS9teS1wcml2YXRlLXRvcGljIiwie3NjaGVtZX06Ly97K2hvc3R9L2RlbW8vYm9va3Mve2lkfS5qc29ubGQiLCIvLndlbGwta25vd24vbWVyY3VyZS9zdWJzY3JpcHRpb25zey90b3BpY317L3N1YnNjcmliZXJ9Il0sInBheWxvYWQiOnsidXNlciI6Imh0dHBzOi8vZXhhbXBsZS5jb20vdXNlcnMvZHVuZ2xhcyIsInJlbW90ZUFkZHIiOiIxMjcuMC4wLjEifX19.z5YrkHwtkz3O_nOnhC_FP7_bmeISe3eykAkGbAl5K7c',
+ Authorization: 'Bearer eyJhbGciOiJIUzI1NiJ9.eyJtZXJjdXJlIjp7InB1Ymxpc2giOlsiKiJdLCJzdWJzY3JpYmUiOlsiaHR0cHM6Ly9leGFtcGxlLmNvbS9teS1wcml2YXRlLXRvcGljIiwie3NjaGVtZX06Ly97K2hvc3R9L2RlbW8vYm9va3Mve2lkfS5qc29ubGQiLCIvLndlbGwta25vd24vbWVyY3VyZS9zdWJzY3JpcHRpb25zey90b3BpY317L3N1YnNjcmliZXJ9Il0sInBheWxvYWQiOnsidXNlciI6Imh0dHBzOi8vZXhhbXBsZS5jb20vdXNlcnMvZHVuZ2xhcyIsInJlbW90ZUFkZHIiOiIxMjcuMC4wLjEifX19.KKPIikwUzRuB3DTpVw6ajzwSChwFw5omBMmMcWKiDcM',
// the JWT must have a mercure.publish key containing an array of topic selectors (can contain "*" for all topics, and be empty for public updates)
// the JWT key must be shared between the hub and the server
'Content-Type': 'application/x-www-form-urlencoded',
@@ -108,7 +110,9 @@ req.end();
// but any HTTP client, written in any language, will be just fine.
```
-The JWT must contain a `publish` property containing an array of topic selectors. This array can be empty to allow publishing anonymous updates only. The topic selector `*` can be used to allow publishing private updates for all topics. To create and read JWTs try [jwt.io](https://jwt.io) ([demo token](https://jwt.io/#debugger-io?token=eyJhbGciOiJIUzI1NiJ9.eyJtZXJjdXJlIjp7InB1Ymxpc2giOlsiKiJdLCJzdWJzY3JpYmUiOlsiaHR0cHM6Ly9leGFtcGxlLmNvbS9teS1wcml2YXRlLXRvcGljIiwie3NjaGVtZX06Ly97K2hvc3R9L2RlbW8vYm9va3Mve2lkfS5qc29ubGQiLCIvLndlbGwta25vd24vbWVyY3VyZS9zdWJzY3JpcHRpb25zey90b3BpY317L3N1YnNjcmliZXJ9Il0sInBheWxvYWQiOnsidXNlciI6Imh0dHBzOi8vZXhhbXBsZS5jb20vdXNlcnMvZHVuZ2xhcyIsInJlbW90ZUFkZHIiOiIxMjcuMC4wLjEifX19.z5YrkHwtkz3O_nOnhC_FP7_bmeISe3eykAkGbAl5K7c), key: `!ChangeMe!`).
+The JWT must contain a `publish` property containing an array of topic selectors.
+This array can be empty to allow publishing anonymous updates only.
+The topic selector `*` can be used to allow publishing private updates for all topics. To create and read JWTs try [jwt.io](https://jwt.io) ([demo token](https://jwt.io/#debugger-io?token=eyJhbGciOiJIUzI1NiJ9.eyJtZXJjdXJlIjp7InB1Ymxpc2giOlsiKiJdLCJzdWJzY3JpYmUiOlsiaHR0cHM6Ly9leGFtcGxlLmNvbS9teS1wcml2YXRlLXRvcGljIiwie3NjaGVtZX06Ly97K2hvc3R9L2RlbW8vYm9va3Mve2lkfS5qc29ubGQiLCIvLndlbGwta25vd24vbWVyY3VyZS9zdWJzY3JpcHRpb25zey90b3BpY317L3N1YnNjcmliZXJ9Il0sInBheWxvYWQiOnsidXNlciI6Imh0dHBzOi8vZXhhbXBsZS5jb20vdXNlcnMvZHVuZ2xhcyIsInJlbW90ZUFkZHIiOiIxMjcuMC4wLjEifX19.KKPIikwUzRuB3DTpVw6ajzwSChwFw5omBMmMcWKiDcM), key: `!ChangeThisMercureHubJWTSecretKey!`).
## Active Subscriptions
diff --git a/docs/hub/cloud.md b/docs/hub/cloud.md
new file mode 100644
index 00000000..409d7f52
--- /dev/null
+++ b/docs/hub/cloud.md
@@ -0,0 +1,62 @@
+# Mercure.rocks Cloud Service
+
+Mercure.rocks provides a managed cloud service enabling instant deployment of hosted hubs, without the need for operational or DevOps skills.
+Our SRE team constantly monitors hubs. Updates are automatically applied.
+
+Unlike the free and open-source hubs, Cloud hubs use [clustering](cluster.md) and are deployed in a high-availability infrastructure.
+
+The Cloud service is built on top of the free and open-source hub and helps fund its development.
+
+## Subscribing
+
+Purchase your managed Mercure.rocks hub [directly online](https://mercure.rocks/pricing)!
+
+After purchase, your hub will be instantly provisionned and available under a `mercure.rocks` subdomain. A TLS certificate is also automatically created.
+
+You'll have access to an administration interface allowing you to:
+
+* configure your hub (the same [configuration settings](config.md) as for the free and open-source hub are available)
+* access to the logs of your hub
+* [set a custom domain name](#custom-domain)
+* [switch to another plan](#switching-between-plans)
+
+## Custom Domain
+
+Your managed hub can be associated with a custom domain. A TLS certificate is also automatically created for your custom domain.
+
+If you use the cookie-based authentication mechanism, it is necessary to associate your hub with a subdomain of your website's main domain [to avoid CORS problems](troubleshooting.md#cors-issues).
+
+To associate your personalized domain with your hub:
+
+1. From the administration interface provided by your domain registrar, add a `CNAME` DNS entry pointing to the domain name ending in `.mercure.rocks.` displayed in the Mercure.rocks administration interface.
+2. Define your custom domain name in the Mercure.rocks administration interface.
+
+The DNS entry and TLS certificate may take up to 24 hours to become available.
+In general, it only takes a few minutes.
+
+## Switching Between Plans
+
+If you need more or fewer resources, you can switch [from one plan to another](https://mercure.rocks/pricing) at any time from the administration interface.
+
+The switch is made without service interruption.
+
+## Rate Limiting
+
+If you reach [your current plan limits](https://mercure.rocks/pricing), the hub will return HTTP status code 429 (`Too Many Requests`).
+
+Publication requests will be rejected and subscription requests will fail.
+
+Be sure to catch and logs these errors in your code.
+
+If you need more requests, upgrade to a higher plan or [the On Premise version](#on-premise).
+
+## On Premise
+
+The [high availability hub](cluster.md) we use for the cloud service can also be hosted on your own infrastructure. When you use the [on-premise version](cluster.md#high-availability-on-premise-version), there are no limits other than the load that can be handled by your servers.
+
+[Contact us for more information on the On Premise version](mailto:contact@mercure.rocks?subject=I%27m%20interested%20in%20Mercure%20on%20premise).
+
+## Support
+
+For support requests related to the Cloud version of Mercure.rocks, send a mail to [contact@mercure.rocks](mailto:contact@mercure.rocks?subject=Cloud%20support%20request).
+Please include the ID of your hub in the message.
diff --git a/docs/hub/cluster.md b/docs/hub/cluster.md
index 444990f9..946ec0f4 100644
--- a/docs/hub/cluster.md
+++ b/docs/hub/cluster.md
@@ -3,26 +3,26 @@
The free version of the Mercure.rocks Hub is shipped with transports (BoltDB and local) that can only run on a single node.
However, the Mercure.rocks Hub has been designed from the ground up to support clusters.
-Both [the managed (starting from the Pro plan) and the High Availability (HA) versions of the Mercure.rocks Hub](https://mercure.rocks/pricing) natively run on multiple nodes.
+Both [the Cloud (starting from the Pro plan) and the On Premise versions of the Mercure.rocks Hub](https://mercure.rocks/pricing) natively run on multiple nodes.
These versions are designed for fault tolerance and can support very high loads.
Both versions work by providing extra transports supporting synchronization of several nodes.
They support all features of the free Hub.
-If you don't want to purchase a managed or an On Premise version of the Mercure.rocks Hub, you can also create your custom build of Mercure.rocks [using a custom transport](https://github.com/dunglas/mercure/blob/main/hub/transport.go#L13-L22).
+If you don't want to purchase a Cloud or an On Premise version of the Mercure.rocks Hub, you can also create your custom build of Mercure.rocks [using a custom transport](https://github.com/dunglas/mercure/blob/main/transport.go#L40-L52).
## Managed Version
-[The managed version](https://mercure.rocks/pricing) is hosted on our own High Availability infrastructure (built on top of Kubernetes). This service is 100% hosted and managed: you have nothing to do!
+[The Cloud version](cloud.md) is hosted on our own High Availability infrastructure (built on top of Kubernetes). This service is 100% hosted and managed: you have nothing to do!
The managed version of the Mercure.rocks Hub can be purchased [directly online](https://mercure.rocks/pricing).
After the purchase, a production-ready Hub is instantly deployed.
To use it, just configure your custom domain name (if any) and your secret JWT key from the administration panel, that's all!
-## High Availability Version
+## High Availability On Premise Version
-The High Availability Mercure.rocks Hub is a drop-in replacement for the free Hub which allows to spread the load across as many servers as you want. It is designed to run on your own servers and is fault tolerant by default.
+The High Availability On Premise Mercure.rocks Hub is a drop-in replacement for the free Hub which allows to spread the load across as many servers as you want. It is designed to run on your own servers and is fault tolerant by default.
The HA version is shipped with transports having node synchronization capabilities.
These transports can rely on:
@@ -37,7 +37,7 @@ We can help you to decide which synchronization mechanism will be the best suite
The HA version is provided as binaries and as a Docker image. We also maintain a Helm chart allowing to install it
on any Kubernetes cluster.
-For more details (and a benchmark), [refer to the case study presented by the iGraal's CTO](https://speakerdeck.com/dunglas/mercure-real-time-for-php-made-easy?slide=52).
+For more details (and a benchmark), [read the case studies section](../spec/use-cases.md#case-studies).
### Purchasing
@@ -51,8 +51,8 @@ This key must be set in an environment variable named `MERCURE_LICENSE`.
Ex:
MERCURE_LICENSE=snip \
- MERCURE_PUBLISHER_JWT_KEY='!ChangeMe!' \
- MERCURE_SUBSCRIBER_JWT_KEY='!ChangeMe!' \
+ MERCURE_PUBLISHER_JWT_KEY='!ChangeThisMercureHubJWTSecretKey!' \
+ MERCURE_SUBSCRIBER_JWT_KEY='!ChangeThisMercureHubJWTSecretKey!' \
./mercure run
If you use the Helm chart, set the `license` value and change the Docker image to use the one provided.
@@ -78,15 +78,14 @@ Most Cloud Computing platforms also provide managed versions of Redis.
##### Configuration
-All the configuration parameters, and formats, supported by the free Mercure.rocks Hub are also available.
-See https://mercure.rocks/docs/hub/config.
+All [the configuration parameters and formats](https://mercure.rocks/docs/hub/config) supported by the free Mercure.rocks Hub are also available.
To use Redis, the `MERCURE_TRANSPORT_URL` environment variable must be set like in this example:
MERCURE_TRANSPORT_URL=redis://127.0.0.1:6379/mercure-ha \
MERCURE_LICENSE=snip \
- MERCURE_PUBLISHER_JWT_KEY='!ChangeMe!' \
- MERCURE_SUBSCRIBER_JWT_KEY='!ChangeMe!' \
+ MERCURE_PUBLISHER_JWT_KEY='!ChangeThisMercureHubJWTSecretKey!' \
+ MERCURE_SUBSCRIBER_JWT_KEY='!ChangeThisMercureHubJWTSecretKey!' \
./mercure run
The following options can be passed as query parameters of the URL set in `transport_url`:
@@ -111,24 +110,23 @@ Most Cloud Computing platforms also provide managed versions of PostgreSQL.
| Presence API | ❌ (planned) |
| Custom event ID | ✅ |
-##### Configuration
+##### PostgreSQL Configuration
-All the configuration parameters, and formats, supported by the free Mercure.rocks Hub are also available.
-See https://mercure.rocks/docs/hub/config.
+All [the configuration parameters and formats](https://mercure.rocks/docs/hub/config) supported by the free Mercure.rocks Hub are also available.
To use PostgreSQL `LISTEN`/`NOTIFY`, the `MERCURE_TRANSPORT_URL` environment variable must be set like in this example:
MERCURE_TRANSPORT_URL=postgres://user:password@127.0.0.1/mercure-ha \
MERCURE_LICENSE=snip \
- MERCURE_PUBLISHER_JWT_KEY='!ChangeMe!' \
- MERCURE_SUBSCRIBER_JWT_KEY='!ChangeMe!' \
+ MERCURE_PUBLISHER_JWT_KEY='!ChangeThisMercureHubJWTSecretKey!' \
+ MERCURE_SUBSCRIBER_JWT_KEY='!ChangeThisMercureHubJWTSecretKey!' \
./mercure run
-The following options can be passed as query parameters of the URL set in `transport_url`: https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNSTRING
+[Options supported by `libpq`](https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNSTRING) can be passed as query parameters of the URL set in `transport_url`.
#### Kafka Transport
-The Kafka transport should only be used when Pulsar is already part of your stack.
+The Kafka transport should only be used when Kafka is already part of your stack.
To install Apache Kafka, [read the quickstart guide](https://kafka.apache.org/quickstart).
@@ -146,17 +144,16 @@ The Mercure.rocks hub has been tested with:
| Presence API | ❌ |
| Custom event ID | ✅ |
-##### Configuration
+##### Kafka Configuration
-All the configuration parameters, and formats, supported by the free Mercure.rocks Hub are also available.
-See https://mercure.rocks/docs/hub/config.
+All [the configuration parameters and formats](https://mercure.rocks/docs/hub/config) supported by the free Mercure.rocks Hub are also available.
To use Kafka, the `MERCURE_TRANSPORT_URL` environment variable must be set like in this example:
MERCURE_TRANSPORT_URL=kafka://kafka/?addr=localhost:9092&topic=mercure-ha \
MERCURE_LICENSE=snip \
- MERCURE_PUBLISHER_JWT_KEY='!ChangeMe!' \
- MERCURE_SUBSCRIBER_JWT_KEY='!ChangeMe!' \
+ MERCURE_PUBLISHER_JWT_KEY='!ChangeThisMercureHubJWTSecretKey!' \
+ MERCURE_SUBSCRIBER_JWT_KEY='!ChangeThisMercureHubJWTSecretKey!' \
./mercure run
The following options can be passed as query parameters of the URL set in `transport_url`:
@@ -182,17 +179,16 @@ To install Apache Pulsar, [read the documentation](https://pulsar.apache.org/doc
| Presence API | ❌ |
| Custom event ID | ❌ (planned) |
-##### Configuration
+##### Pulsar Configuration
-All the configuration parameters, and formats, supported by the free Mercure.rocks Hub are also available.
-See https://mercure.rocks/docs/hub/config.
+All [the configuration parameters and formats](https://mercure.rocks/docs/hub/config) supported by the free Mercure.rocks Hub are also available.
To use Pulsar, the `MERCURE_TRANSPORT_URL` environment variable must be set like in this example:
MERCURE_TRANSPORT_URL=pulsar://localhost:6650?topic=mercure-ha&subscription_name=the-node-id \
MERCURE_LICENSE=snip \
- MERCURE_PUBLISHER_JWT_KEY='!ChangeMe!' \
- MERCURE_SUBSCRIBER_JWT_KEY='!ChangeMe!' \
+ MERCURE_PUBLISHER_JWT_KEY='!ChangeThisMercureHubJWTSecretKey!' \
+ MERCURE_SUBSCRIBER_JWT_KEY='!ChangeThisMercureHubJWTSecretKey!' \
./mercure run
The following options can be passed as query parameters of the URL set in `transport_url`:
@@ -204,9 +200,13 @@ The following options can be passed as query parameters of the URL set in `trans
### Docker Images and Kubernetes Chart
-An official Docker image and [a Kubernetes Chart](https://github.com/helm/charts/tree/master/stable/mercure) are available.
+An official Docker image and [a Kubernetes Chart](install.md#kubernetes) are available.
Contact us if you need help to use them.
### Updates
New releases of the High Availability Mercure.rocks Hub are automatically available available in the Amazon S3 bucket containing the binary and on the Docker registry.
+
+## Support
+
+For support requests related to the On Premise version of Mercure.rocks, send a mail to [contact@mercure.rocks](mailto:contact@mercure.rocks?subject=On%20Premise%20support%20request).
diff --git a/docs/hub/config.md b/docs/hub/config.md
index 2711d174..a608a9fe 100644
--- a/docs/hub/config.md
+++ b/docs/hub/config.md
@@ -11,31 +11,23 @@ A minimal `Caddyfile` for the Mercure hub looks like this:
```Caddyfile
# The address of your server
-localhost
-
-route {
- mercure {
- # Publisher JWT key
- publisher_jwt !ChangeMe!
- # Subscriber JWT key
- subscriber_jwt !ChangeMe!
- }
-
- respond "Not Found" 404
+localhost {
+ mercure {
+ # Publisher JWT key
+ publisher_jwt !ChangeThisMercureHubJWTSecretKey!
+ # Subscriber JWT key
+ subscriber_jwt !ChangeThisMercureHubJWTSecretKey!
+ }
+
+ respond "Not Found" 404
}
```
Caddy will automatically generate a Let's Encrypt TLS certificate for you! So you can use HTTPS.
-To disable HTTPS entirely, set the [`auto_https`](https://caddyserver.com/docs/caddyfile/options#auto-https) global option to off:
+To disable HTTPS, prefix the name of the server by `http://`:
```Caddyfile
-{
- auto_https off
-}
-
-my-domain.test:3000
-
-route {
+http://my-domain.test:3000 {
# ...
}
```
@@ -59,8 +51,9 @@ The following Mercure-specific directives are available:
| `transport_url ` | URL representation of the transport to use. Use `local://local` to disabled history, (example `bolt:///var/run/mercure.db?size=100&cleanup_frequency=0.4`), see also [the cluster mode](cluster.md) | `bolt://mercure.db` |
| `dispatch_timeout ` | maximum duration of the dispatch of a single update, set to `0s` disable | `5s` |
| `write_timeout ` | maximum duration before closing the connection, set to `0s` disable | `600s` |
-| `ui` | enable the UI and expose demo endpoints | |
-| `demo` | enable the UI but do not expose demo endpoints | |
+| `protocol_version_compatibility` | version of the protocol to be backward compatible with (only version 7 is supported) | disabled |
+| `demo` | enable the UI and expose demo endpoints | |
+| `ui` | enable the UI but do not expose demo endpoints | |
| `cache ` | cache configuration (see [Ristretto's docs](https://github.com/dgraph-io/ristretto)), set to -1 to disable the cache | `6e7` `1e8` (100MB) |
See also [the list of built-in Caddyfile directives](https://caddyserver.com/docs/caddyfile/directives).
@@ -69,17 +62,36 @@ See also [the list of built-in Caddyfile directives](https://caddyserver.com/doc
The provided `Caddyfile` and the Docker image provide convenient environment variables:
-| Environment variable | Description | Default value |
-| ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | ------------------- |
-| `GLOBAL_OPTIONS` | the [global options block](https://caddyserver.com/docs/caddyfile/options#global-options) to inject in the `Caddyfile`, one per line |
-| `SERVER_NAME` | the server name or address | `localhost` |
-| `MERCURE_TRANSPORT_URL` | the value passed to the `transport_url` directive | `bolt://mercure.db` |
-| `MERCURE_PUBLISHER_JWT_KEY` | the JWT key to use for publishers | |
-| `MERCURE_PUBLISHER_JWT_ALG` | the JWT algorithm to use for publishers | `HS256` |
-| `MERCURE_SUBSCRIBER_JWT_KEY` | the JWT key to use for subscribers | |
-| `MERCURE_SUBSCRIBER_JWT_ALG` | the JWT algorithm to use for subscribers | `HS256` |
-| `MERCURE_EXTRA_DIRECTIVES` | a list of extra Mercure directives inject in the Caddy file, one per line | |
-| `MERCURE_LICENSE` | the license to use ([only applicable for the HA version](cluster.md)) | |
+| Environment variable | Description | Default value |
+| ------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------- |
+| `GLOBAL_OPTIONS` | the [global options block](https://caddyserver.com/docs/caddyfile/options#global-options) to inject in the `Caddyfile`, one per line | |
+| `CADDY_EXTRA_CONFIG` | the [snippet](https://caddyserver.com/docs/caddyfile/concepts#snippets) or the [named-routes](https://caddyserver.com/docs/caddyfile/concepts#named-routes) options block to inject in the `Caddyfile`, one per line | |
+| `CADDY_SERVER_EXTRA_DIRECTIVES`| [`Caddyfile` directives](https://caddyserver.com/docs/caddyfile/concepts#directives) | |
+| `SERVER_NAME` | the server name or address | `localhost` |
+| `MERCURE_TRANSPORT_URL` | the value passed to the `transport_url` directive | `bolt://mercure.db` |
+| `MERCURE_PUBLISHER_JWT_KEY` | the JWT key to use for publishers | |
+| `MERCURE_PUBLISHER_JWT_ALG` | the JWT algorithm to use for publishers | `HS256` |
+| `MERCURE_SUBSCRIBER_JWT_KEY` | the JWT key to use for subscribers | |
+| `MERCURE_SUBSCRIBER_JWT_ALG` | the JWT algorithm to use for subscribers | `HS256` |
+| `MERCURE_EXTRA_DIRECTIVES` | a list of extra [Mercure directives](#directives) inject in the Caddy file, one per line | |
+| `MERCURE_LICENSE` | the license to use ([only applicable for the HA version](cluster.md)) | |
+
+## HealthCheck
+
+The Mercure.rocks Hub provides a `/healthz` endpoint that returns a `200 OK` status code if the server is healthy.
+
+Here is an example of how to use the health check in a Docker Compose file:
+```yaml
+# compose.yaml
+services:
+ mercure:
+ # ...
+ healthcheck:
+ test: ["CMD", "curl", "-f", "https://localhost/healthz"]
+ timeout: 5s
+ retries: 5
+ start_period: 60s
+```
## JWT Verification
@@ -89,40 +101,46 @@ Here is an example of how to use environments variables with a RSA public key.
Generate keys (if you don't already have them):
- ssh-keygen -t rsa -b 4096 -m PEM -f publisher.key
- openssl rsa -in publisher.key -pubout -outform PEM -out publisher.key.pub
-
- ssh-keygen -t rsa -b 4096 -m PEM -f subscriber.key
- openssl rsa -in subscriber.key -pubout -outform PEM -out subscriber.key.pub
+```console
+ssh-keygen -t rsa -b 4096 -m PEM -f publisher.key
+openssl rsa -in publisher.key -pubout -outform PEM -out publisher.key.pub
+
+ssh-keygen -t rsa -b 4096 -m PEM -f subscriber.key
+openssl rsa -in subscriber.key -pubout -outform PEM -out subscriber.key.pub
+```
Start the hub:
- MERCURE_PUBLISHER_JWT_KEY=$(cat publisher.key.pub) \
- MERCURE_PUBLISHER_JWT_ALG=RS256 \
- MERCURE_SUBSCRIBER_JWT_KEY=$(cat subscriber.key.pub) \
- MERCURE_SUBSCRIBER_JWT_ALG=RS256 \
- ./mercure run
+```console
+MERCURE_PUBLISHER_JWT_KEY=$(cat publisher.key.pub) \
+MERCURE_PUBLISHER_JWT_ALG=RS256 \
+MERCURE_SUBSCRIBER_JWT_KEY=$(cat subscriber.key.pub) \
+MERCURE_SUBSCRIBER_JWT_ALG=RS256 \
+./mercure run
+```
## Bolt Adapter
The [Data Source Name (DSN)](https://en.wikipedia.org/wiki/Data_source_name) specifies the path to the [bolt](https://github.com/etcd-io/bbolt) database as well as options
-| Parameter | Description
-|---------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
-| `bucket_name` | name of the bolt bucket to store events. default to `updates` |
+| Parameter | Description |
+|---------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| `bucket_name` | name of the bolt bucket to store events. default to `updates` |
| `cleanup_frequency` | chances to trigger history cleanup when an update occurs, must be a number between `0` (never cleanup) and `1` (cleanup after every publication), default to `0.3`. |
-| `size` | size of the history (to retrieve lost messages using the `Last-Event-ID` header), set to `0` to never remove old events (default) |
+| `size` | size of the history (to retrieve lost messages using the `Last-Event-ID` header), set to `0` to never remove old events (default) |
Below are common examples of valid DSNs showing a combination of available values:
- # absolute path to `updates.db`
- transport_url bolt:///var/run/database.db
+```Caddyfile
+# absolute path to `updates.db`
+transport_url bolt:///var/run/database.db
- # path to `updates.db` in the current directory
- transport_url bolt://database.db
+# path to `updates.db` in the current directory
+transport_url bolt://database.db
- # custom options
- transport_url bolt://database.db?bucket_name=demo&size=1000&cleanup_frequency=0.5
+# custom options
+transport_url bolt://database.db?bucket_name=demo&size=1000&cleanup_frequency=0.5
+```
You can visualize and edit the content of the database using [boltdbweb](https://github.com/evnix/boltdbweb).
@@ -130,10 +148,10 @@ You can visualize and edit the content of the database using [boltdbweb](https:/
**The legacy server is deprecated and will be removed in the next version. Consider upgrading to the Caddy-based build.**
-The legacy Mercure.rocks Hub is configurable using [environment variables](https://en.wikipedia.org/wiki/Environment_variable) (recommended in production, [twelve-factor app methodology](https://12factor.net/)), command line flags and configuration files (JSON, TOML, YAML, HCL, envfile and Java properties files are supported).
+The legacy Mercure.rocks Hub is configurable using [environment variables](https://en.wikipedia.org/wiki/Environment_variable) (recommended in production, [twelve-factor app methodology](https://12factor.net/)), command-line flags and configuration files (JSON, TOML, YAML, HCL, envfile and Java properties files are supported).
Environment variables must be the name of the configuration parameter in uppercase.
-Run `./mercure -h` to see all available command line flags.
+Run `./mercure -h` to see all available command-line flags.
Configuration files must be named `mercure.` (ex: `mercure.yaml`) and stored in one of the following directories:
@@ -145,35 +163,36 @@ Most configuration parameters are hot reloaded: changes made to environment vari
When using environment variables, list must be space separated. As flags parameters, they must be comma separated.
-| Parameter | Description | Default |
-|----------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------|
-| `acme_cert_dir` | the directory where to store Let's Encrypt certificates | |
-| `acme_hosts` | a list of hosts for which Let's Encrypt certificates must be issued | |
-| `acme_http01_addr` | the address used by the acme server to listen on (example: `0.0.0.0:8080`) | `:http` |
-| `addr` | the address to listen on (example: `127.0.0.1:3000`. Note that Let's Encrypt only supports the default port: to use Let's Encrypt, **do not set this parameter**. | `:http` or `:https` depending if HTTPS is enabled or not |
-| `allow_anonymous` | allow subscribers with no valid JWT to connect | `false` |
-| `cert_file` | a cert file (to use a custom certificate) | |
-| `key_file` | a key file (to use a custom certificate) | |
-| `compress` | Use HTTP compression | `false` |
-| `cors_allowed_origins` | a space-separated list of allowed CORS origins, can be `*` for all | |
-| `debug` | debug mode, **dangerous, don't enable in production** (logs updates' content, why an update is not send to a specific subscriber and recovery stack traces) | `false` |
-| `demo` | demo mode (automatically enabled when `debug` is `true`) and enables ui at `https://example.com/.well-known/mercure/ui/` | `false` |
-| `dispatch_timeout` | maximum duration of the dispatch of a single update, set to `0s` to disable | `5s` |
-| `subscriptions` | expose the subscription web API and dispatch private updates when a subscription between the Hub and a subscriber is established or closed. The topic follows the template `/.well-known/mercure/subscriptions/{topicSelector}/{subscriberID}` | `false` |
-| `heartbeat_interval` | interval between heartbeats (useful with some proxies, and old browsers), set to `0s` to disable | `40s` |
-| `jwt_key` | the JWT key to use for both publishers and subscribers | |
-| `jwt_algorithm` | the JWT verification algorithm to use for both publishers and subscribers, e.g. `HS256` or `RS512` | `HS256` |
-| `metrics_enabled` | Enable the `/metrics` HTTP endpoint. Provide metrics for Hub monitoring in the OpenMetrics (Prometheus) format | `false` |
-| `metrics_addr` | the address to listen on | `127.0.0.1:9764` |
-| `publish_allowed_origins` | a list of origins allowed to publish (only applicable when using cookie-based auth) | |
-| `publisher_jwt_key` | must contain the secret key to valid publishers' JWT, can be omitted if `jwt_key` is set | |
-| `publisher_jwt_algorithm` | the JWT verification algorithm to use for publishers, e.g. `HS256` or `RS512` | `HS256` |
-| `read_timeout` | maximum duration for reading the entire request, including the body, set to `0s` to disable | `5s` |
-| `subscriber_jwt_key` | must contain the secret key to valid subscribers' JWT, can be omitted if `jwt_key` is set | |
-| `subscriber_jwt_algorithm` | the JWT verification algorithm to use for subscribers, e.g. `HS256` or `RS512` | `HS256` |
-| `transport_url` | URL representation of the history database. Provided database are `null` to disable history, `bolt` to use [bbolt](https://github.com/etcd-io/bbolt) (example `bolt:///var/run/mercure.db?size=100&cleanup_frequency=0.4`) | `bolt://updates.db` |
-| `use_forwarded_headers` | use the `X-Forwarded-For`, and `X-Real-IP` for the remote (client) IP address, `X-Forwarded-Proto` or `X-Forwarded-Scheme` for the scheme (http or https), `X-Forwarded-Host` for the host and the RFC 7239 `Forwarded` header, which may include both client IPs and schemes. If this option is enabled, the reverse proxy must override or remove these headers or you will be at risk | `false` |
-| `write_timeout` | maximum duration before closing the connection, set to `0s` to disable | `600s` |
+| Parameter | Description | Default |
+|----------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------|
+| `acme_cert_dir` | the directory where to store Let's Encrypt certificates | |
+| `acme_hosts` | a list of hosts for which Let's Encrypt certificates must be issued | |
+| `acme_http01_addr` | the address used by the acme server to listen on (example: `0.0.0.0:8080`) | `:http` |
+| `addr` | the address to listen on (example: `127.0.0.1:3000`. Note that Let's Encrypt only supports the default port: to use Let's Encrypt, **do not set this parameter**. | `:http` or `:https` depending if HTTPS is enabled or not |
+| `allow_anonymous` | allow subscribers with no valid JWT to connect | `false` |
+| `cert_file` | a cert file (to use a custom certificate) | |
+| `key_file` | a key file (to use a custom certificate) | |
+| `compress` | Use HTTP compression | `false` |
+| `cors_allowed_origins` | a space-separated list of allowed CORS origins, can be `*` for all | |
+| `debug` | debug mode, **dangerous, don't enable in production** (logs updates' content, why an update is not send to a specific subscriber and recovery stack traces) | `false` |
+| `demo` | demo mode (automatically enabled when `debug` is `true`) and enables ui at `https://example.com/.well-known/mercure/ui/` | `false` |
+| `dispatch_timeout` | maximum duration of the dispatch of a single update, set to `0s` to disable | `5s` |
+| `subscriptions` | expose the subscription web API and dispatch private updates when a subscription between the Hub and a subscriber is established or closed. The topic follows the template `/.well-known/mercure/subscriptions/{topicSelector}/{subscriberID}` | `false` |
+| `heartbeat_interval` | interval between heartbeats (useful with some proxies, and old browsers), set to `0s` to disable | `40s` |
+| `jwt_key` | the JWT key to use for both publishers and subscribers | |
+| `jwt_algorithm` | the JWT verification algorithm to use for both publishers and subscribers, e.g. `HS256` or `RS512` | `HS256` |
+| `metrics_enabled` | Enable the `/metrics` HTTP endpoint. Provide metrics for Hub monitoring in the OpenMetrics (Prometheus) format | `false` |
+| `metrics_addr` | the address to listen on | `127.0.0.1:9764` |
+| `publish_allowed_origins` | a list of origins allowed to publish (only applicable when using cookie-based auth) | |
+| `publisher_jwt_key` | must contain the secret key to valid publishers' JWT, can be omitted if `jwt_key` is set | |
+| `publisher_jwt_algorithm` | the JWT verification algorithm to use for publishers, e.g. `HS256` or `RS512` | `HS256` |
+| `read_timeout` | maximum duration for reading the entire request, including the body, set to `0s` to disable | `5s` |
+| `read__header_timeout` | the amount of time allowed to read request headers, set to `0s` to disable | `5s` |
+| `subscriber_jwt_key` | must contain the secret key to valid subscribers' JWT, can be omitted if `jwt_key` is set | |
+| `subscriber_jwt_algorithm` | the JWT verification algorithm to use for subscribers, e.g. `HS256` or `RS512` | `HS256` |
+| `transport_url` | URL representation of the history database. Provided database are `null` to disable history, `bolt` to use [bbolt](https://github.com/etcd-io/bbolt) (example `bolt:///var/run/mercure.db?size=100&cleanup_frequency=0.4`) | `bolt://updates.db` |
+| `use_forwarded_headers` | use the `X-Forwarded-For`, and `X-Real-IP` for the remote (client) IP address, `X-Forwarded-Proto` or `X-Forwarded-Scheme` for the scheme (`http` or `https`), `X-Forwarded-Host` for the host and the RFC 7239 `Forwarded` header, which may include both client IPs and schemes. If this option is enabled, the reverse proxy must override or remove these headers or you will be at risk | `false` |
+| `write_timeout` | maximum duration before closing the connection, set to `0s` to disable | `600s` |
If `acme_hosts` or both `cert_file` and `key_file` are provided, an HTTPS server supporting HTTP/2 connection will be started.
If not, an HTTP server will be started (**not secure**).
diff --git a/docs/hub/cookbooks.md b/docs/hub/cookbooks.md
index b19a4fe5..9931ff95 100644
--- a/docs/hub/cookbooks.md
+++ b/docs/hub/cookbooks.md
@@ -12,28 +12,11 @@ You may also be interested in spreading the load across several servers using [t
To reproduce the problem, we provide [a load test](load-test.md) that you can use to stress your infrastructure.
-## Using NGINX as an HTTP/2 Reverse Proxy in Front of the Hub
-
-[NGINX](https://www.nginx.com) is supported out of the box. Use the following proxy configuration:
-
-```nginx
-server {
- listen 443 ssl http2;
- listen [::]:443 ssl http2;
-
- ssl_certificate /path/to/ssl/cert.crt;
- ssl_certificate_key /path/to/ssl/cert.key;
-
- location / {
- proxy_pass http://url-of-your-mercure-hub;
- proxy_read_timeout 24h;
- proxy_http_version 1.1;
- proxy_set_header Connection "";
-
- ## Be sure to set USE_FORWARDED_HEADERS=1 to allow the hub to use those headers ##
- proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
- proxy_set_header X-Forwarded-Host $host;
- proxy_set_header X-Forwarded-Proto $scheme;
- }
-}
-```
+
+## Using Mercure Behind a Reverse Proxy
+
+Mercure hubs run perfectly well behind reverse proxies.
+Here are some configuration for popular proxies:
+
+* [NGINX](nginx.md)
+* [Traefik](traefik.md)
diff --git a/docs/hub/debug.md b/docs/hub/debug.md
index 043e0df2..c4a26a9d 100644
--- a/docs/hub/debug.md
+++ b/docs/hub/debug.md
@@ -8,7 +8,7 @@ To enable the profiler, add the `debug` global directive to your `Caddyfile`:
```Caddyfile
{
- debug
+ debug
}
# ...
diff --git a/docs/hub/install.md b/docs/hub/install.md
index ed7b2b96..9cd6a9ca 100644
--- a/docs/hub/install.md
+++ b/docs/hub/install.md
@@ -1,8 +1,9 @@
-# Install the Mercure.rocks hub
+# Install the Mercure.rocks Hub
## Managed and HA Versions
-[Managed and High Availability versions of Mercure.rocks](https://mercure.rocks/pricing) are available, give them a try!
+The easiest way to get started with Mercure is to subscribe to the [Cloud version](https://mercure.rocks/pricing).
+Give it a try!
## Prebuilt Binary
@@ -10,17 +11,21 @@ The Mercure.rocks hub is available as a custom build of the [Caddy web server](h
First, download the archive corresponding to your operating system and architecture [from the release page](https://github.com/dunglas/mercure/releases), extract the archive and open a shell in the resulting directory.
-*Note:* Mac OS users must use the `Darwin` binary.
+*Note:* macOS users must download the `Darwin` binary, then run `xattr -d com.apple.quarantine ./mercure` [to release the hub from quarantine](troubleshooting.md#macos-localhost-installation-error).
-Then, to start the Mercure.rocks Hub in development mode on Linux and Mac OS X, run:
+To start the Mercure.rocks Hub in development mode on Linux and macOS, run:
- MERCURE_PUBLISHER_JWT_KEY='!ChangeMe!' \
- MERCURE_SUBSCRIBER_JWT_KEY='!ChangeMe!' \
- ./mercure run -config Caddyfile.dev
+```console
+MERCURE_PUBLISHER_JWT_KEY='!ChangeThisMercureHubJWTSecretKey!' \
+MERCURE_SUBSCRIBER_JWT_KEY='!ChangeThisMercureHubJWTSecretKey!' \
+./mercure run --config Caddyfile.dev
+```
On Windows, start PowerShell, go into the extracted directory and run:
- $env:MERCURE_PUBLISHER_JWT_KEY='!ChangeMe!'; $env:MERCURE_SUBSCRIBER_JWT_KEY='!ChangeMe!'; .\mercure.exe run -config Caddyfile.dev
+```powershell
+$env:MERCURE_PUBLISHER_JWT_KEY='!ChangeThisMercureHubJWTSecretKey!'; $env:MERCURE_SUBSCRIBER_JWT_KEY='!ChangeThisMercureHubJWTSecretKey!'; .\mercure.exe run --config Caddyfile.dev
+```
*Note:* The Windows Defender Firewall will ask you if you want to allow `mercure.exe` to communicate through it.
Allow it for both public and private networks. If you use an antivirus, or another firewall software, be sure to whitelist `mercure.exe`.
@@ -32,9 +37,11 @@ In development mode, anonymous subscribers are allowed and the debug UI is avail
To run the server in production mode, run this command:
- MERCURE_PUBLISHER_JWT_KEY='!ChangeMe!' \
- MERCURE_SUBSCRIBER_JWT_KEY='!ChangeMe!' \
- ./mercure run
+```console
+MERCURE_PUBLISHER_JWT_KEY='!ChangeThisMercureHubJWTSecretKey!' \
+MERCURE_SUBSCRIBER_JWT_KEY='!ChangeThisMercureHubJWTSecretKey!' \
+./mercure run
+```
In production mode, the debugger UI is disabled and anonymous subscribers aren't allowed.
To change these default settings, [learn how to configure the Mercure.rocks hub](config.md).
@@ -52,33 +59,39 @@ To compile the development version, see [https://github.com/dunglas/mercure/blob
A Docker image is available on Docker Hub. The following command is enough to get a working server in development mode:
- docker run \
- -e MERCURE_PUBLISHER_JWT_KEY='!ChangeMe!' \
- -e MERCURE_SUBSCRIBER_JWT_KEY='!ChangeMe!' \
- -p 80:80 \
- -p 443:443 \
- dunglas/mercure caddy run -config /etc/caddy/Caddyfile.dev
+```console
+docker run \
+ -e MERCURE_PUBLISHER_JWT_KEY='!ChangeThisMercureHubJWTSecretKey!' \
+ -e MERCURE_SUBSCRIBER_JWT_KEY='!ChangeThisMercureHubJWTSecretKey!' \
+ -p 80:80 \
+ -p 443:443 \
+ dunglas/mercure caddy run --config /etc/caddy/Caddyfile.dev
+```
The server is then available on `https://localhost`. Anonymous subscribers are allowed and the debugger UI is available on `https://localhost/.well-known/mercure/ui/`.
In production, simply run:
- docker run \
- -e MERCURE_PUBLISHER_JWT_KEY='!ChangeMe!' \
- -e MERCURE_SUBSCRIBER_JWT_KEY='!ChangeMe!' \
- -p 80:80 \
- -p 443:443 \
- dunglas/mercure
+```console
+docker run \
+ -e MERCURE_PUBLISHER_JWT_KEY='!ChangeThisMercureHubJWTSecretKey!' \
+ -e MERCURE_SUBSCRIBER_JWT_KEY='!ChangeThisMercureHubJWTSecretKey!' \
+ -p 80:80 \
+ -p 443:443 \
+ dunglas/mercure
+```
HTTPS support is automatically enabled. If you run the Mercure hub behind a reverse proxy [such as NGINX](cookbooks.md#using-nginx-as-an-http-2-reverse-proxy-in-front-of-the-hub), you usually want to use unencrypted HTTP.
This can be done like that:
- docker run \
- -e SERVER_NAME=':80' \
- -e MERCURE_PUBLISHER_JWT_KEY='!ChangeMe!' \
- -e MERCURE_SUBSCRIBER_JWT_KEY='!ChangeMe!' \
- -p 80:80 \
- dunglas/mercure
+```console
+docker run \
+ -e SERVER_NAME=':80' \
+ -e MERCURE_PUBLISHER_JWT_KEY='!ChangeThisMercureHubJWTSecretKey!' \
+ -e MERCURE_SUBSCRIBER_JWT_KEY='!ChangeThisMercureHubJWTSecretKey!' \
+ -p 80:80 \
+ dunglas/mercure
+```
The Docker image is based on [the Caddy Server Docker image](https://registry.hub.docker.com/_/caddy).
See [the configuration section](config.md) and [the documentation of the Docker image for Caddy](https://registry.hub.docker.com/_/caddy) to learn how to configure it to fit your needs.
@@ -89,57 +102,66 @@ Use [the Helm package manager](https://helm.sh/) to install Mercure on a Kuberne
To install the chart with the release name `my-release`, run the following commands:
- helm repo add mercure https://charts.mercure.rocks
- helm install my-release mercure/mercure
+```console
+helm repo add mercure https://charts.mercure.rocks
+helm install my-release mercure/mercure
+```
See [the list of available values](https://github.com/dunglas/mercure/blob/main/charts/mercure/README.md) for this chart.
## Docker Compose
-If you prefer to use `docker-compose` to run the Mercure.rocks hub, here's a sample service definition:
+If you prefer to use `docker compose` to run the Mercure.rocks hub, here's a sample service definition:
```yaml
-# docker-compose.yml
-version: "3.7"
-
+# compose.yaml
services:
- caddy:
+ mercure:
image: dunglas/mercure
restart: unless-stopped
environment:
# Uncomment the following line to disable HTTPS
#SERVER_NAME: ':80'
- MERCURE_PUBLISHER_JWT_KEY: '!ChangeMe!'
- MERCURE_SUBSCRIBER_JWT_KEY: '!ChangeMe!'
+ MERCURE_PUBLISHER_JWT_KEY: '!ChangeThisMercureHubJWTSecretKey!'
+ MERCURE_SUBSCRIBER_JWT_KEY: '!ChangeThisMercureHubJWTSecretKey!'
# Uncomment the following line to enable the development mode
- #command: /usr/bin/caddy run -config /etc/caddy/Caddyfile.dev
+ #command: /usr/bin/caddy run --config /etc/caddy/Caddyfile.dev
+ healthcheck:
+ test: ["CMD", "curl", "-f", "https://localhost/healthz"]
+ timeout: 5s
+ retries: 5
+ start_period: 60s
ports:
- - "80:80"
- - "443:443"
+ - '80:80'
+ - '443:443'
volumes:
- - caddy_data:/data
- - caddy_config:/config
+ - mercure_data:/data
+ - mercure_config:/config
volumes:
- caddy_data:
- caddy_config:
+ mercure_data:
+ mercure_config:
```
+Alternatively, you may want to [run the Mercure.rocks hub behind Traefik Proxy](traefik.md).
+
## Arch Linux
Mercure.rocks is available [on the AUR](https://aur.archlinux.org/packages/mercure), you can install it with your favorite AUR wrapper:
- yay -S mercure
-
+```console
+yay -S mercure
+```
Or download the `PKGBUILD` and compile and install it: `makepkg -sri`.
## Custom Caddy Build
It's also possible to [download Caddy with Mercure and other modules included](https://caddyserver.com/download?package=github.com%2Fdunglas%2Fmercure%2Fcaddy), or to build your own binaries using [`xcaddy`](https://github.com/caddyserver/xcaddy):
- xcaddy build \
- --with github.com/dunglas/mercure \
- --with github.com/dunglas/mercure/caddy
+```console
+xcaddy build \
+ --with github.com/dunglas/mercure/caddy
+```
## Integrations in Popular Frameworks
diff --git a/docs/hub/load-test.md b/docs/hub/load-test.md
index 812b5ae4..788ec5ed 100644
--- a/docs/hub/load-test.md
+++ b/docs/hub/load-test.md
@@ -27,3 +27,7 @@ Available environment variables (all are optional):
* `CONNECTION_DURATION`: duration of subscribers' connection
* `RANDOM_CONNECTION_DURATION`: to randomize the connection duration (will longs `CONNECTION_DURATION` at max)
* `PRIVATE_UPDATES`: to send private updates with random topics instead of public updates always with the same topic
+
+## See Also
+
+* [Conformance tests](../ecosystem/conformance-tests.md)
diff --git a/docs/hub/nginx.md b/docs/hub/nginx.md
new file mode 100644
index 00000000..c5b1e6e8
--- /dev/null
+++ b/docs/hub/nginx.md
@@ -0,0 +1,25 @@
+# Using NGINX as an HTTP/2 Reverse Proxy in Front of the Hub
+
+[NGINX](https://www.nginx.com) is supported out of the box. Use the following proxy configuration:
+
+```nginx
+server {
+ listen 443 ssl http2;
+ listen [::]:443 ssl http2;
+
+ ssl_certificate /path/to/ssl/cert.crt;
+ ssl_certificate_key /path/to/ssl/cert.key;
+
+ location / {
+ proxy_pass http://url-of-your-mercure-hub;
+ proxy_read_timeout 24h;
+ proxy_http_version 1.1;
+ proxy_set_header Connection "";
+
+ ## Be sure to set USE_FORWARDED_HEADERS=1 to allow the hub to use those headers ##
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Host $host;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ }
+}
+```
diff --git a/docs/hub/traefik.md b/docs/hub/traefik.md
new file mode 100644
index 00000000..df856b39
--- /dev/null
+++ b/docs/hub/traefik.md
@@ -0,0 +1,50 @@
+# Use the Mercure.rocks Hub with Traefik Proxy
+
+[Traefik](https://doc.traefik.io/traefik/) is a free and open source *edge router* poular in the Docker and Kubernetes ecosystems.
+
+The following Docker Compose file exposes a Mercure.rocks hub through Traefik:
+
+```yaml
+# compose.yaml
+services:
+ reverse-proxy:
+ # The official v2 Traefik image
+ image: traefik:v2.10
+ command: --api.insecure=true --providers.docker
+ ports:
+ # The HTTP port
+ - '80:80'
+ # The Web UI (enabled by --api.insecure=true)
+ - '8080:8080'
+ volumes:
+ # So that Traefik can listen to the Docker events
+ - /var/run/docker.sock:/var/run/docker.sock
+
+ mercure:
+ # The official Mercure image
+ image: dunglas/mercure
+ restart: unless-stopped
+ environment:
+ # Disables Mercure.rocks auto-HTTPS feature, HTTPS must be handled at edge by Traefik or another proxy in front of it
+ SERVER_NAME: ':80'
+ MERCURE_PUBLISHER_JWT_KEY: '!ChangeThisMercureHubJWTSecretKey!'
+ MERCURE_SUBSCRIBER_JWT_KEY: '!ChangeThisMercureHubJWTSecretKey!'
+ # Enables the development mode, comment the following line to run the hub in prod mode
+ command: /usr/bin/caddy run --config /etc/caddy/Caddyfile.dev
+ healthcheck:
+ test: ["CMD", "curl", "-f", "https://localhost/healthz"]
+ timeout: 5s
+ retries: 5
+ start_period: 60s
+ volumes:
+ - mercure_data:/data
+ - mercure_config:/config
+ labels:
+ - "traefik.http.routers.mercure.rule=Host(`mercure.docker.localhost`)"
+
+volumes:
+ mercure_data:
+ mercure_config:
+```
+
+Refer to the Traefik Proxy documentation to learn about all features provided by Traefik.
diff --git a/docs/hub/troubleshooting.md b/docs/hub/troubleshooting.md
index 920c9996..b41e2785 100644
--- a/docs/hub/troubleshooting.md
+++ b/docs/hub/troubleshooting.md
@@ -7,29 +7,58 @@
* Check the logs written by the hub on `stderr`, they contain the exact reason why the token has been rejected
* Be sure to set a **secret key** (and not a JWT) in `JWT_KEY` (or in `SUBSCRIBER_JWT_KEY` and `PUBLISHER_JWT_KEY`)
* If the secret key contains special characters, be sure to escape them properly, especially if you set the environment variable in a shell, or in a YAML file (Kubernetes...)
-* The publisher always needs a valid JWT, even if the `anonymous` directive is present in the `Caddyfile`, this JWT **must** have a property named `publish`. To dispatch private updates, the `publish` property must contain the list of topic selectors this publisher can use ([example](https://jwt.io/#debugger-io?token=eyJhbGciOiJIUzI1NiJ9.eyJtZXJjdXJlIjp7InB1Ymxpc2giOlsiKiJdLCJzdWJzY3JpYmUiOlsiaHR0cHM6Ly9leGFtcGxlLmNvbS9teS1wcml2YXRlLXRvcGljIiwie3NjaGVtZX06Ly97K2hvc3R9L2RlbW8vYm9va3Mve2lkfS5qc29ubGQiLCIvLndlbGwta25vd24vbWVyY3VyZS9zdWJzY3JpcHRpb25zey90b3BpY317L3N1YnNjcmliZXJ9Il0sInBheWxvYWQiOnsidXNlciI6Imh0dHBzOi8vZXhhbXBsZS5jb20vdXNlcnMvZHVuZ2xhcyIsInJlbW90ZUFkZHIiOiIxMjcuMC4wLjEifX19.z5YrkHwtkz3O_nOnhC_FP7_bmeISe3eykAkGbAl5K7c))
-* The subscriber needs a valid JWT only if the `anonymous` directive isn't present in the `Caddyfile`, or to subscribe to private updates, in this case the JWT **must** have a property named `subscribe` and containing an array of topic selectors ([example](https://jwt.io/#debugger-io?token=eyJhbGciOiJIUzI1NiJ9.eyJtZXJjdXJlIjp7InB1Ymxpc2giOlsiKiJdLCJzdWJzY3JpYmUiOlsiaHR0cHM6Ly9leGFtcGxlLmNvbS9teS1wcml2YXRlLXRvcGljIiwie3NjaGVtZX06Ly97K2hvc3R9L2RlbW8vYm9va3Mve2lkfS5qc29ubGQiLCIvLndlbGwta25vd24vbWVyY3VyZS9zdWJzY3JpcHRpb25zey90b3BpY317L3N1YnNjcmliZXJ9Il0sInBheWxvYWQiOnsidXNlciI6Imh0dHBzOi8vZXhhbXBsZS5jb20vdXNlcnMvZHVuZ2xhcyIsInJlbW90ZUFkZHIiOiIxMjcuMC4wLjEifX19.z5YrkHwtkz3O_nOnhC_FP7_bmeISe3eykAkGbAl5K7c))
+* The publisher always needs a valid JWT, even if the `anonymous` directive is present in the `Caddyfile`, this JWT **must** have a property named `publish`. To dispatch private updates, the `publish` property must contain the list of topic selectors this publisher can use ([example](https://jwt.io/#debugger-io?token=eyJhbGciOiJIUzI1NiJ9.eyJtZXJjdXJlIjp7InB1Ymxpc2giOlsiKiJdLCJzdWJzY3JpYmUiOlsiaHR0cHM6Ly9leGFtcGxlLmNvbS9teS1wcml2YXRlLXRvcGljIiwie3NjaGVtZX06Ly97K2hvc3R9L2RlbW8vYm9va3Mve2lkfS5qc29ubGQiLCIvLndlbGwta25vd24vbWVyY3VyZS9zdWJzY3JpcHRpb25zey90b3BpY317L3N1YnNjcmliZXJ9Il0sInBheWxvYWQiOnsidXNlciI6Imh0dHBzOi8vZXhhbXBsZS5jb20vdXNlcnMvZHVuZ2xhcyIsInJlbW90ZUFkZHIiOiIxMjcuMC4wLjEifX19.KKPIikwUzRuB3DTpVw6ajzwSChwFw5omBMmMcWKiDcM))
+* The subscriber needs a valid JWT only if the `anonymous` directive isn't present in the `Caddyfile`, or to subscribe to private updates, in this case the JWT **must** have a property named `subscribe` and containing an array of topic selectors ([example](eyJhbGciOiJIUzI1NiJ9.eyJtZXJjdXJlIjp7InB1Ymxpc2giOlsiKiJdLCJzdWJzY3JpYmUiOlsiaHR0cHM6Ly9leGFtcGxlLmNvbS9teS1wcml2YXRlLXRvcGljIiwie3NjaGVtZX06Ly97K2hvc3R9L2RlbW8vYm9va3Mve2lkfS5qc29ubGQiLCIvLndlbGwta25vd24vbWVyY3VyZS9zdWJzY3JpcHRpb25zey90b3BpY317L3N1YnNjcmliZXJ9Il0sInBheWxvYWQiOnsidXNlciI6Imh0dHBzOi8vZXhhbXBsZS5jb20vdXNlcnMvZHVuZ2xhcyIsInJlbW90ZUFkZHIiOiIxMjcuMC4wLjEifX19.KKPIikwUzRuB3DTpVw6ajzwSChwFw5omBMmMcWKiDcM))
For both the `publish` property, the array can be empty to publish only public updates. For both `publish` and `subscribe`, you can use `["*"]` to match all topics.
## CORS Issues
-If the app connecting to the Mercure hub and the hub itself are not served from the same domain, you must whitelist the domain of the app using the CORS (Cross-Origin Resource Sharing) mechanism. The usual symptoms of a CORS misconfiguration are errors about missing CORS HTTP headers in the console of the browser (`Refused to connect to 'https://hub.example.com/.well-known/mercure?topic=foo' because it violates the following Content Security Policy directive` with Chrome, `Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at https://hub.example.com/.well-known/mercure?topic=foo. (Reason: CORS header ‘Access-Control-Allow-Origin’ missing)` with Firefox).
+If the app connecting to the Mercure hub and the hub itself are not served from the same domain, you must whitelist the domain of the app using the CORS (Cross-Origin Resource Sharing) mechanism.
+
+The usual symptoms of a CORS misconfiguration are errors about missing CORS HTTP headers in the console of the browser:
+
+* Chrome: `Refused to connect to 'https://hub.example.com/.well-known/mercure?topic=foo' because it violates the following Content Security Policy directive`
+* Firefox: `Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at https://hub.example.com/.well-known/mercure?topic=foo. (Reason: CORS header ‘Access-Control-Allow-Origin’ missing)`
To fix these errors, set the list of domains allowed to connect to the hub as value of the `cors_origins` in the `Caddyfile`. Example: `cors_origins https://example.com https://example.net`. Don't forget the `https://` prefix before the domain name!
-If you use an authorization mechanism (cookie or `Authorization` header), [you cannot set the value of `cors_origins` to `*`](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#Credentialed_requests_and_wildcards). You **must** explicitly set the list of allowed origins.
-If you don't use an authorization mechanism (anonymous mode), you can set the value of `cors_origins` to `*` to allow all applications to connect to the hub (be sure to understand the security implications of what you are doing).
+If you use an authorization mechanism (cookie or `Authorization` header), [you **cannot** set the value of `cors_origins` to `*`](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#Credentialed_requests_and_wildcards). You **must** explicitly set the list of allowed origins.
-If you use the Symfony CLI and want to use an external hub, be sure to read [this issue](https://github.com/symfony/cli/issues/424).
+If you don't use an authorization mechanism (anonymous mode), you can set the value of `cors_origins` to `*` to allow all applications to connect to the hub (be sure to understand the security implications of what you are doing).
## URI Templates and Topics
Try [our URI template tester](https://uri-template-tester.mercure.rocks/) to ensure that the template matches the topic.
-## Mac OS Localhost Installation Error
+## Disconnection With the Inability To Reconnect After Some Time
+
+If the JWT supplied to the Mercury hub contains [an `exp` (expiration time) claim](https://www.rfc-editor.org/rfc/rfc7519#section-4.1.3) (this is the default for tokens generated with most JWT libraries), the hub will automatically disconnect when the expiry date is reached.
+After that, it is no longer possible to reconnect with the same JWT, as it has expired. The hub will return an HTTP 401 error.
+
+One solution is to generate a new, valid JWT before reconnecting.
+
+Although not setting the `exp` claim allows an open-ended connection, this solution is not recommended as it reduces the security of your data (someone with a valid JWT will be able to connect forever, at least until the secret key is changed).
+
+## macOS Localhost Installation Error
+
+To execute the Mercure.rocks binary, you must first [release it from quarantine](https://eclecticlight.co/2023/03/13/ventura-has-changed-app-quarantine-with-a-new-xattr/).
+
+### macOS Ventura And Later Versions
+
+Remove the `com.apple.quarantine` attribute:
+
+ xattr -d com.apple.quarantine ./mercure
+
+You can now start the hub as usual:
+
+ ./mercure run
+
+The attribute only needs to be deleted once.
+
+### macOS Catalina
-How to process for Mercure to work in Mac OS Catalina:
+On macOS Catalina and later versions, follow these steps:
* In the Finder on your Mac, locate the app that you want to open
* Control-click on the app icon, then choose "Open" from the shortcut menu
diff --git a/docs/mercure.md b/docs/mercure.md
index 917e06b8..db86bd62 100644
--- a/docs/mercure.md
+++ b/docs/mercure.md
@@ -1,23 +1,36 @@
# Mercure in a Few Words
-The Mercure protocol:
+Mercure is an open solution for real-time communications designed to be fast, reliable and battery-efficient. It is a modern and convenient replacement for both the Websocket API and the higher-level libraries and services relying on it.
+
+Mercure is especially useful to add streaming and asynchronous capabilities to REST and GraphQL APIs. Because it is a thin layer on top of HTTP and SSE, Mercure is natively supported by modern web browsers, mobile applications and IoT devices.
+
+A free (as in beer, and as in speech) reference server, a commercial High Availability version and a hosted service are available.
+
+![Subscriptions Schema](../spec/subscriptions.png)
+
+## The Protocol
* native browser support, no lib nor SDK required (built on top of HTTP and [server-sent events](https://www.smashingmagazine.com/2018/02/sse-websockets-data-flow-http2/))
* compatible with all existing servers, even those who don't support persistent connections (serverless architecture, PHP, FastCGI...)
* built-in connection re-establishment and state reconciliation
* [JWT](https://jwt.io/)-based authorization mechanism (securely dispatch an update to some selected subscribers)
-* performant, leverages [HTTP/2 multiplexing](https://developers.google.com/web/fundamentals/performance/http2/#request_and_response_multiplexing)
+* performant, leverages [HTTP multiplexing](https://web.dev/performance-http2/#request-and-response-multiplexing)
* designed with [hypermedia in mind](https://en.wikipedia.org/wiki/HATEOAS), also supports [GraphQL](https://graphql.org/)
* auto-discoverable through [web linking](https://tools.ietf.org/html/rfc5988)
* message encryption support
* can work with old browsers (IE7+) using an `EventSource` polyfill
* [connection-less push](https://html.spec.whatwg.org/multipage/server-sent-events.html#eventsource-push) in controlled environments (e.g. browsers on mobile handsets tied to specific carriers)
-The Mercure.rocks hub implementation:
+[Read the specification](../spec/mercure.md)
+
+## The Hub
* Fast, written in Go
-* Works everywhere: static binaries and Docker images available
-* Automatic HTTP/2 and HTTPS (using Let's Encrypt) support
+* Works everywhere: [static binaries](hub/install.md#prebuilt-binary), [Docker image](hub/install.md#docker-image) and [Kubernetes chart](hub/install.md#kubernetes)
+* Automatic HTTP/2, HTTP/3 and HTTPS (using Let's Encrypt) support
+* [Clustering and High Availability support](hub/cluster.md)
* CORS support, CSRF protection mechanism
* Cloud Native, follows [the Twelve-Factor App](https://12factor.net) methodology
-* Open source (AGPL)
+* Free and Open source (AGPL), SaaS, and commercial versions available
+
+[Get your hub](hub/install.md)
diff --git a/docs/spec/faq.md b/docs/spec/faq.md
index b780a665..b6774593 100644
--- a/docs/spec/faq.md
+++ b/docs/spec/faq.md
@@ -5,7 +5,7 @@
In a nutshell [the WebSocket API](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API) is low level, Mercure is a high level.
Mercure provides convenient built-in features such as authorization, re-connection, state reconciliation and a presence API ; while with WebSockets, you need to implement them yourself.
-Also WebSockets [are not designed to leverage HTTP/2+](https://www.infoq.com/articles/websocket-and-http2-coexist) and are known to be [hard to secure](https://gravitational.com/blog/kubernetes-websocket-upgrade-security-vulnerability/). On the other hand Mercure relies on plain HTTP connections and benefits from the performance an security improvement built in the latest versions of this protocol.
+Also WebSockets [are not designed to leverage HTTP/2+](https://www.infoq.com/articles/websocket-and-http2-coexist) and are known to be [hard to secure](https://gravitational.com/blog/kubernetes-websocket-upgrade-security-vulnerability/). On the other hand Mercure relies on plain HTTP connections and benefits from the performance an security improvement built-in the latest versions of this protocol.
HTTP/2 connections are multiplexed and bidirectional by default (it was not the case of HTTP/1).
When using Mercure over a h2 connection (recommended), your app can receive data through Server-Sent Events, and send data to the server with regular `POST` (or `PUT`/`PATCH`/`DELETE`) requests, with no overhead.
@@ -43,7 +43,7 @@ Because they are delivery agnostic, Mercure plays particularly well with [GraphQ
For instance, [the API Platform framework has native support for GraphQL subscriptions thanks to Mercure](https://api-platform.com/docs/master/core/graphql/#subscriptions).
In response to the subscription query, the GraphQL server may return a corresponding topic URL.
-The client can then subscribe to the Mercure's event stream corresponding to this subscription by creating a new `EventSource` with an URL like `https://example.com/.well-known/mercure?topic=https://example.com/subscriptions/` as parameter.
+The client can then subscribe to the Mercure's event stream corresponding to this subscription by creating a new `EventSource` with a URL like `https://example.com/.well-known/mercure?topic=https://example.com/subscriptions/` as parameter.
Updates for the given subscription can then be sent from the GraphQL server to the clients through the Mercure hub (in the `data` property of the server-sent event).
diff --git a/docs/spec/use-cases.md b/docs/spec/use-cases.md
index 71b6b029..71a3462c 100644
--- a/docs/spec/use-cases.md
+++ b/docs/spec/use-cases.md
@@ -1,27 +1,35 @@
-# Use Cases
+# Case Studies and Use Cases
From building a chat room to expose a fully-featured event store, Mercure covers a broad spectrum of use cases related to realtime and async APIs.
+## Case Studies
+
+* [How Raven Controls is using Mercury to power major events such as Cop 21 and Euro 2020](https://api-platform.com/con/2022/conferences/real-time-and-beyond-with-mercure/)
+* [Pushing 8 million of Mercure notifications per day to run mail.tm](https://les-tilleuls.coop/en/blog/mail-tm-mercure-rocks-and-api-platform)
+* [100,000 simultaneous Mercure users to power iGraal](https://speakerdeck.com/dunglas/mercure-real-time-for-php-made-easy?slide=52)
+
Example of usage: the Mercure integration in [API Platform](https://api-platform.com/docs/client-generator):
-![API Platform screencast](https://api-platform.com/d20c0f7f49b5655a3788d9c570c1c80a/client-generator-demo.gif)
+![API Platform screencast](https://raw.githubusercontent.com/api-platform/docs/3.1/create-client/images/create-client-demo.gif)
+
+## Use Cases
Here are some popular use cases:
-## Live Availability
+### Live Availability
* a webapp retrieves the availability status of a product from a REST API and displays it: only one is still available
* 3 minutes later, the last product is bought by another customer
* the webapp's view instantly shows that this product isn't available anymore
-## Asynchronous Jobs
+### Asynchronous Jobs
* a webapp tells the server to compute a report, this task is costly and will take some time to complete
* the server delegates the computation of the report to an asynchronous worker (using message queue), and closes the connection with the webapp
* the worker sends the report to the webapp when it is computed
-## Collaborative Editing
+### Collaborative Editing
* a webapp allows several users to edit the same document concurrently
* changes made are immediately broadcasted to all connected users
diff --git a/example_test.go b/example_test.go
index 19a4e439..6ac78832 100644
--- a/example_test.go
+++ b/example_test.go
@@ -7,6 +7,7 @@ import (
"github.com/dunglas/mercure"
)
+//nolint:gosec
func Example() {
h, err := mercure.NewHub(
mercure.WithPublisherJWT([]byte("!ChangeMe!"), "HS256"),
diff --git a/examples/chat/Dockerfile b/examples/chat/Dockerfile
index cec5cf1f..98d92307 100644
--- a/examples/chat/Dockerfile
+++ b/examples/chat/Dockerfile
@@ -1,7 +1,10 @@
-FROM tiangolo/meinheld-gunicorn:python3.8-alpine3.11
+FROM tiangolo/meinheld-gunicorn:python3.9
+
+WORKDIR /app
COPY ./requirements.txt /app
-RUN pip install -r requirements.txt
+RUN pip install --no-cache-dir -r requirements.txt
+
COPY ./main.py .
COPY ./static static
COPY ./templates templates
diff --git a/examples/chat/chart/mercure-example-chat/README.md b/examples/chat/chart/mercure-example-chat/README.md
index a9946ada..2a391954 100644
--- a/examples/chat/chart/mercure-example-chat/README.md
+++ b/examples/chat/chart/mercure-example-chat/README.md
@@ -25,7 +25,7 @@ A minimalist chat system, using Mercure and the Flask microframework to handle c
| ingress.hosts[0].host | string | `"chart-example.local"` | |
| ingress.hosts[0].paths | list | `[]` | |
| ingress.tls | list | `[]` | |
-| jwtKey | string | `"!ChangeMe!"` | |
+| jwtKey | string | `"!ChangeThisMercureHubJWTSecretKey!"` | |
| messageUriTemplate | string | `"https://chat.example.com/messages/{id}"` | |
| nameOverride | string | `""` | |
| nodeSelector | object | `{}` | |
@@ -42,4 +42,4 @@ A minimalist chat system, using Mercure and the Flask microframework to handle c
| tolerations | list | `[]` | |
----------------------------------------------
-Autogenerated from chart metadata using [helm-docs v1.5.0](https://github.com/norwoodj/helm-docs/releases/v1.5.0)
+Autogenerated from chart metadata using [helm-docs v1.13.1](https://github.com/norwoodj/helm-docs/releases/v1.13.1)
diff --git a/examples/chat/chart/mercure-example-chat/values.yaml b/examples/chat/chart/mercure-example-chat/values.yaml
index 085bde76..1c2a1963 100644
--- a/examples/chat/chart/mercure-example-chat/values.yaml
+++ b/examples/chat/chart/mercure-example-chat/values.yaml
@@ -12,7 +12,7 @@ image:
hubUrl: https://demo.mercure.rocks/.well-known/mercure
cookieDomain: .mercure.rocks
-jwtKey: "!ChangeMe!"
+jwtKey: "!ChangeThisMercureHubJWTSecretKey!"
messageUriTemplate: https://chat.example.com/messages/{id}
imagePullSecrets: []
diff --git a/examples/chat/main.py b/examples/chat/main.py
index 46267030..f3efc773 100644
--- a/examples/chat/main.py
+++ b/examples/chat/main.py
@@ -22,50 +22,66 @@
COOKIE_DOMAIN: the cookie domain (default: None)
"""
-from flask import Flask, make_response, request, render_template, abort
-import jwt
import os
-import json
+
+import jwt
+from flask import Flask, abort, make_response, render_template, request
from uritemplate import expand
-HUB_URL = os.environ.get(
- 'HUB_URL', 'https://localhost/.well-known/mercure')
-JWT_KEY = os.environ.get('JWT_KEY', '!ChangeMe!')
+HUB_URL = os.environ.get("HUB_URL", "https://localhost/.well-known/mercure")
+JWT_KEY = os.environ.get("JWT_KEY", "!ChangeThisMercureHubJWTSecretKey!")
MESSAGE_URI_TEMPLATE = os.environ.get(
- 'MESSAGE_URI_TEMPLATE', 'https://chat.example.com/messages/{id}')
+ "MESSAGE_URI_TEMPLATE", "https://chat.example.com/messages/{id}"
+)
-SUBSCRIPTIONS_TEMPLATE = '/.well-known/mercure/subscriptions/{topic}{/subscriber}'
+SUBSCRIPTIONS_TEMPLATE = "/.well-known/mercure/subscriptions/{topic}{/subscriber}"
SUBSCRIPTIONS_TOPIC = expand(SUBSCRIPTIONS_TEMPLATE, topic=MESSAGE_URI_TEMPLATE)
app = Flask(__name__)
-@app.route('/', methods=['GET'])
+@app.route("/", methods=["GET"])
def join():
- return render_template('join.html')
+ return render_template("join.html")
-@app.route('/', methods=['POST'])
+@app.route("/", methods=["POST"])
def chat():
- username = request.form['username']
+ username = request.form["username"]
if not username:
abort(400)
token = jwt.encode(
- {'mercure':
- {
- 'subscribe': [MESSAGE_URI_TEMPLATE, SUBSCRIPTIONS_TEMPLATE],
- 'publish': [MESSAGE_URI_TEMPLATE],
- 'payload': {'username': username}
+ {
+ "mercure": {
+ "subscribe": [MESSAGE_URI_TEMPLATE, SUBSCRIPTIONS_TEMPLATE],
+ "publish": [MESSAGE_URI_TEMPLATE],
+ "payload": {"username": username},
}
- },
+ },
JWT_KEY,
- algorithm='HS256',
+ algorithm="HS256",
)
- resp = make_response(render_template('chat.html', config={
- 'hubURL': HUB_URL, 'messageURITemplate': MESSAGE_URI_TEMPLATE, 'subscriptionsTopic': SUBSCRIPTIONS_TOPIC, 'username': username}))
- resp.set_cookie('mercureAuthorization', token, httponly=True, path='/.well-known/mercure',
- samesite="strict", domain=os.environ.get('COOKIE_DOMAIN', None), secure=request.is_secure) # Force secure to True for real apps
+ resp = make_response(
+ render_template(
+ "chat.html",
+ config={
+ "hubURL": HUB_URL,
+ "messageURITemplate": MESSAGE_URI_TEMPLATE,
+ "subscriptionsTopic": SUBSCRIPTIONS_TOPIC,
+ "username": username,
+ },
+ )
+ )
+ resp.set_cookie(
+ "mercureAuthorization",
+ token,
+ httponly=True,
+ path="/.well-known/mercure",
+ samesite="strict",
+ domain=os.environ.get("COOKIE_DOMAIN", None),
+ secure=request.is_secure,
+ ) # Force secure to True for real apps
return resp
diff --git a/examples/chat/static/app.css b/examples/chat/static/app.css
index 013c0096..b5b63836 100644
--- a/examples/chat/static/app.css
+++ b/examples/chat/static/app.css
@@ -1,19 +1,20 @@
.hero img {
- width: 40%;
+ width: 40%;
}
-#chat #messages, #chat .panel {
- height: 100%;
- overflow-y: scroll;
+#chat #messages,
+#chat .panel {
+ height: 100%;
+ overflow-y: scroll;
}
#chat .columns {
- height: 70vh;
+ height: 70vh;
}
#chat .box {
- display: flex;
- flex-direction: column;
- justify-content: space-between;
- height: 100%;
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+ height: 100%;
}
diff --git a/examples/chat/static/chat.js b/examples/chat/static/chat.js
index e8128e6e..fb9488ba 100644
--- a/examples/chat/static/chat.js
+++ b/examples/chat/static/chat.js
@@ -1,105 +1,107 @@
-const type = "https://chat.example.com/Message";
+/* eslint-env browser */
+
+const type = 'https://chat.example.com/Message'
const { hubURL, messageURITemplate, subscriptionsTopic, username } = JSON.parse(
- document.getElementById("config").textContent
-);
+ document.getElementById('config').textContent
+)
-document.getElementById("username").textContent = username;
+document.getElementById('username').textContent = username
-const $messages = document.getElementById("messages");
-const $messageTemplate = document.getElementById("message");
-const $userList = document.getElementById("user-list");
-const $onlineUserTemplate = document.getElementById("online-user");
+const $messages = document.getElementById('messages')
+const $messageTemplate = document.getElementById('message')
+const $userList = document.getElementById('user-list')
+const $onlineUserTemplate = document.getElementById('online-user')
-let userList, es;
+let userList;
(async () => {
const resp = await fetch(new URL(subscriptionsTopic, hubURL), {
- credentials: "include",
- });
- const subscriptionCollection = await resp.json();
+ credentials: 'include'
+ })
+ const subscriptionCollection = await resp.json()
userList = new Map(
subscriptionCollection.subscriptions
.reduce((acc, { payload }) => {
- if (payload.username != username) acc.push([payload.username, true]);
- return acc;
+ if (payload.username !== username) acc.push([payload.username, true])
+ return acc
}, [])
.sort()
- );
- updateUserListView();
+ )
+ updateUserListView()
- const subscribeURL = new URL(hubURL);
+ const subscribeURL = new URL(hubURL)
subscribeURL.searchParams.append(
- "Last-Event-ID",
+ 'lastEventID',
subscriptionCollection.lastEventID
- );
- subscribeURL.searchParams.append("topic", messageURITemplate);
+ )
+ subscribeURL.searchParams.append('topic', messageURITemplate)
subscribeURL.searchParams.append(
- "topic",
+ 'topic',
`${subscriptionsTopic}{/subscriber}`
- );
+ )
- const es = new EventSource(subscribeURL, { withCredentials: true });
+ const es = new EventSource(subscribeURL, { withCredentials: true })
es.onmessage = ({ data }) => {
- const update = JSON.parse(data);
+ const update = JSON.parse(data)
- if (update["@type"] === type) {
- displayMessage(update);
- return;
+ if (update['@type'] === type) {
+ displayMessage(update)
+ return
}
- if (update["type"] === "Subscription") {
- updateUserList(update);
- return;
+ if (update.type === 'Subscription') {
+ updateUserList(update)
+ return
}
- console.warn("Received an unsupported update type", update);
- };
-})();
+ console.warn('Received an unsupported update type', update)
+ }
+})()
const updateUserListView = () => {
- $userList.textContent = "";
+ $userList.textContent = ''
userList.forEach((_, username) => {
- const el = document.importNode($onlineUserTemplate.content, true);
- el.querySelector(".username").textContent = username;
- $userList.append(el);
- });
-};
+ const el = document.importNode($onlineUserTemplate.content, true)
+ el.querySelector('.username').textContent = username
+ $userList.append(el)
+ })
+}
const displayMessage = ({ username, message }) => {
- const el = document.importNode($messageTemplate.content, true);
- el.querySelector(".username").textContent = username;
- el.querySelector(".msg").textContent = message;
- $messages.append(el);
+ const el = document.importNode($messageTemplate.content, true)
+ el.querySelector('.username').textContent = username
+ el.querySelector('.msg').textContent = message
+ $messages.append(el)
// scroll at the bottom when a new message is received
- $messages.scrollTop = $messages.scrollHeight;
-};
+ $messages.scrollTop = $messages.scrollHeight
+}
const updateUserList = ({ active, payload }) => {
- if (username === payload.username) return;
+ if (username === payload.username) return
- active ? userList.set(payload.username, true) : userList.delete(payload.username);
- userList = new Map([...userList.entries()].sort());
+ active ? userList.set(payload.username, true) : userList.delete(payload.username)
+ userList = new Map([...userList.entries()].sort())
- updateUserListView();
-};
+ updateUserListView()
+}
-document.querySelector("form").onsubmit = function (e) {
- e.preventDefault();
+document.querySelector('form').onsubmit = function (e) {
+ e.preventDefault()
- const uid = window.crypto.getRandomValues(new Uint8Array(10)).join("");
- const messageTopic = messageURITemplate.replace("{id}", uid);
+ const uid = window.crypto.getRandomValues(new Uint8Array(10)).join('')
+ const messageTopic = messageURITemplate.replace('{id}', uid)
const body = new URLSearchParams({
data: JSON.stringify({
- "@type": type,
- "@id": messageTopic,
- username: username,
- message: this.elements.message.value,
+ '@type': type,
+ '@id': messageTopic,
+ username,
+ message: this.elements.message.value
}),
topic: messageTopic,
- private: true,
- });
- fetch(hubURL, { method: "POST", body, credentials: "include" });
- this.elements.message.value = "";
- this.elements.message.focus();
-};
+ private: true
+ })
+ fetch(hubURL, { method: 'POST', body, credentials: 'include' })
+ this.elements.message.value = ''
+ this.elements.message.focus()
+}
diff --git a/examples/publish/node.js b/examples/publish/node.js
index c925938a..f3e0e677 100644
--- a/examples/publish/node.js
+++ b/examples/publish/node.js
@@ -1,35 +1,35 @@
-const http = require("http");
-const querystring = require("querystring");
+const http = require('http')
+const querystring = require('querystring')
const demoJwt =
- "eyJhbGciOiJIUzI1NiJ9.eyJtZXJjdXJlIjp7InB1Ymxpc2giOlsiKiJdLCJzdWJzY3JpYmUiOlsiaHR0cHM6Ly9leGFtcGxlLmNvbS9teS1wcml2YXRlLXRvcGljIiwie3NjaGVtZX06Ly97K2hvc3R9L2RlbW8vYm9va3Mve2lkfS5qc29ubGQiLCIvLndlbGwta25vd24vbWVyY3VyZS9zdWJzY3JpcHRpb25zey90b3BpY317L3N1YnNjcmliZXJ9Il0sInBheWxvYWQiOnsidXNlciI6Imh0dHBzOi8vZXhhbXBsZS5jb20vdXNlcnMvZHVuZ2xhcyIsInJlbW90ZUFkZHIiOiIxMjcuMC4wLjEifX19.z5YrkHwtkz3O_nOnhC_FP7_bmeISe3eykAkGbAl5K7c";
+ 'eyJhbGciOiJIUzI1NiJ9.eyJtZXJjdXJlIjp7InB1Ymxpc2giOlsiKiJdLCJzdWJzY3JpYmUiOlsiaHR0cHM6Ly9leGFtcGxlLmNvbS9teS1wcml2YXRlLXRvcGljIiwie3NjaGVtZX06Ly97K2hvc3R9L2RlbW8vYm9va3Mve2lkfS5qc29ubGQiLCIvLndlbGwta25vd24vbWVyY3VyZS9zdWJzY3JpcHRpb25zey90b3BpY317L3N1YnNjcmliZXJ9Il0sInBheWxvYWQiOnsidXNlciI6Imh0dHBzOi8vZXhhbXBsZS5jb20vdXNlcnMvZHVuZ2xhcyIsInJlbW90ZUFkZHIiOiIxMjcuMC4wLjEifX19.KKPIikwUzRuB3DTpVw6ajzwSChwFw5omBMmMcWKiDcM'
const postData = querystring.stringify({
- topic: "https://localhost/demo/books/1.jsonld",
- data: JSON.stringify({ key: "updated value" }),
-});
+ topic: 'https://localhost/demo/books/1.jsonld',
+ data: JSON.stringify({ key: 'updated value' })
+})
const req = http.request(
{
- hostname: "localhost",
- port: "3000",
- path: "/.well-known/mercure",
- method: "POST",
+ hostname: 'localhost',
+ port: '3000',
+ path: '/.well-known/mercure',
+ method: 'POST',
headers: {
Authorization: `Bearer ${demoJwt}`,
- "Content-Type": "application/x-www-form-urlencoded",
- "Content-Length": Buffer.byteLength(postData),
- },
+ 'Content-Type': 'application/x-www-form-urlencoded',
+ 'Content-Length': Buffer.byteLength(postData)
+ }
},
(res) => {
- console.log(`Status: ${res.statusCode}`);
- console.log(`Headers: ${JSON.stringify(res.headers)}`);
+ console.log(`Status: ${res.statusCode}`)
+ console.log(`Headers: ${JSON.stringify(res.headers)}`)
}
-);
+)
-req.on("error", (e) => {
- console.error(`An error occurred: ${e.message}`);
-});
+req.on('error', (e) => {
+ console.error(`An error occurred: ${e.message}`)
+})
-req.write(postData);
-req.end();
+req.write(postData)
+req.end()
diff --git a/examples/publish/php.php b/examples/publish/php.php
index 50b5bc2d..36571fcd 100644
--- a/examples/publish/php.php
+++ b/examples/publish/php.php
@@ -1,6 +1,6 @@
'https://localhost/demo/books/1.jsonld',
@@ -9,6 +9,6 @@
echo file_get_contents('https://localhost/.well-known/mercure', false, stream_context_create(['http' => [
'method' => 'POST',
- 'header' => "Content-type: application/x-www-form-urlencoded\r\nAuthorization: Bearer ".DEMO_JWT,
+ 'header' => "Content-type: application/x-www-form-urlencoded\r\nAuthorization: Bearer " . DEMO_JWT,
'content' => $postData,
]]));
diff --git a/examples/publish/ruby.rb b/examples/publish/ruby.rb
index 316e0adf..f47ed682 100644
--- a/examples/publish/ruby.rb
+++ b/examples/publish/ruby.rb
@@ -1,13 +1,15 @@
-require 'json'
-require 'net/http'
+# frozen_string_literal: true
-token = 'eyJhbGciOiJIUzI1NiJ9.eyJtZXJjdXJlIjp7InB1Ymxpc2giOlsiKiJdLCJzdWJzY3JpYmUiOlsiaHR0cHM6Ly9leGFtcGxlLmNvbS9teS1wcml2YXRlLXRvcGljIiwie3NjaGVtZX06Ly97K2hvc3R9L2RlbW8vYm9va3Mve2lkfS5qc29ubGQiLCIvLndlbGwta25vd24vbWVyY3VyZS9zdWJzY3JpcHRpb25zey90b3BpY317L3N1YnNjcmliZXJ9Il0sInBheWxvYWQiOnsidXNlciI6Imh0dHBzOi8vZXhhbXBsZS5jb20vdXNlcnMvZHVuZ2xhcyIsInJlbW90ZUFkZHIiOiIxMjcuMC4wLjEifX19.z5YrkHwtkz3O_nOnhC_FP7_bmeISe3eykAkGbAl5K7c'
+require "json"
+require "net/http"
-Net::HTTP.start('localhost', 3000) do |http|
- req = Net::HTTP::Post.new('/.well-known/mercure')
- req['Authorization'] = "Bearer #{token}"
+token = "eyJhbGciOiJIUzI1NiJ9.eyJtZXJjdXJlIjp7InB1Ymxpc2giOlsiKiJdLCJzdWJzY3JpYmUiOlsiaHR0cHM6Ly9leGFtcGxlLmNvbS9teS1wcml2YXRlLXRvcGljIiwie3NjaGVtZX06Ly97K2hvc3R9L2RlbW8vYm9va3Mve2lkfS5qc29ubGQiLCIvLndlbGwta25vd24vbWVyY3VyZS9zdWJzY3JpcHRpb25zey90b3BpY317L3N1YnNjcmliZXJ9Il0sInBheWxvYWQiOnsidXNlciI6Imh0dHBzOi8vZXhhbXBsZS5jb20vdXNlcnMvZHVuZ2xhcyIsInJlbW90ZUFkZHIiOiIxMjcuMC4wLjEifX19.KKPIikwUzRuB3DTpVw6ajzwSChwFw5omBMmMcWKiDcM"
+
+Net::HTTP.start("localhost", 3000) do |http|
+ req = Net::HTTP::Post.new("/.well-known/mercure")
+ req["Authorization"] = "Bearer #{token}"
req.form_data = {
- topic: 'https://localhost/demo/books/1.jsonld',
+ topic: "https://localhost/demo/books/1.jsonld",
data: { key: :value }.to_json
}
req = http.request(req)
diff --git a/examples/subscribe/python.py b/examples/subscribe/python.py
index 6e4269a1..83408c15 100644
--- a/examples/subscribe/python.py
+++ b/examples/subscribe/python.py
@@ -1,17 +1,18 @@
-from sseclient import SSEClient
-import jwt
import os
+import jwt
+from sseclient import SSEClient
+
token = jwt.encode(
- {'mercure': {'subscribe': ['*']}},
- os.environ.get('JWT_KEY', '!ChangeMe!'),
- algorithm='HS256',
+ {"mercure": {"subscribe": ["*"]}},
+ os.environ.get("JWT_KEY", "!ChangeThisMercureHubJWTSecretKey!"),
+ algorithm="HS256",
)
updates = SSEClient(
- os.environ.get('HUB_URL', 'https://localhost/.well-known/mercure'),
- params={'topic': ['*']},
- headers={'Authorization': b'Bearer '+token},
+ os.environ.get("HUB_URL", "https://localhost/.well-known/mercure"),
+ params={"topic": ["*"]},
+ headers={"Authorization": b"Bearer " + token},
)
for update in updates:
print("Update received: ", update)
diff --git a/gatling/LoadTest.scala b/gatling/LoadTest.scala
index b00f081a..24ae165f 100644
--- a/gatling/LoadTest.scala
+++ b/gatling/LoadTest.scala
@@ -1,19 +1,24 @@
/** Load test for Mercure.
*
- * 1. Grab Gatling 3 on https://gatling.io
- * 2. Run path/to/gatling/bin/gatling.sh --simulations-folder .
+ * 1. Grab Gatling 3 on https://gatling.io 2. Run
+ * path/to/gatling/bin/gatling.sh --simulations-folder .
*
* Available environment variables (all optional):
* - HUB_URL: the URL of the hub to test
- * - JWT: the JWT to use for authenticating the publisher, fallbacks to JWT if not set and PRIVATE_UPDATES set
- * - INITIAL_SUBSCRIBERS: the number of concurrent subscribers initially connected
- * - SUBSCRIBERS_RATE_FROM: minimum rate (per second) of additional subscribers to connect
- * - SUBSCRIBERS_RATE_TO: maximum rate (per second) of additional subscribers to connect
+ * - JWT: the JWT to use for authenticating the publisher, fallbacks to JWT
+ * if not set and PRIVATE_UPDATES set
+ * - INITIAL_SUBSCRIBERS: the number of concurrent subscribers initially
+ * connected
+ * - SUBSCRIBERS_RATE_FROM: minimum rate (per second) of additional
+ * subscribers to connect
+ * - SUBSCRIBERS_RATE_TO: maximum rate (per second) of additional subscribers
+ * to connect
* - PUBLISHERS_RATE_FROM: minimum rate (per second) of publications
* - PUBLISHERS_RATE_TO: maximum rate (per second) of publications
* - INJECTION_DURATION: duration of the publishers injection
* - CONNECTION_DURATION: duration of subscribers' connection
- * - RANDOM_CONNECTION_DURATION: to randomize the connection duration (will longs CONNECTION_DURATION at max)
+ * - RANDOM_CONNECTION_DURATION: to randomize the connection duration (will
+ * longs CONNECTION_DURATION at max)
*/
package mercure
@@ -32,10 +37,12 @@ class LoadTest extends Simulation {
/** JWT to use to publish */
val Jwt = Properties.envOrElse(
"JWT",
- "eyJhbGciOiJIUzI1NiJ9.eyJtZXJjdXJlIjp7InB1Ymxpc2giOlsiKiJdLCJzdWJzY3JpYmUiOlsiaHR0cHM6Ly9leGFtcGxlLmNvbS97aWR9Iiwie3NjaGVtZX06Ly97K2hvc3R9L2RlbW8vYm9va3Mve2lkfS5qc29ubGQiLCIvLndlbGwta25vd24vbWVyY3VyZS9zdWJzY3JpcHRpb25zey90b3BpY317L3N1YnNjcmliZXJ9Il0sInBheWxvYWQiOnsidXNlciI6Imh0dHBzOi8vZXhhbXBsZS5jb20vdXNlcnMvZHVuZ2xhcyIsInJlbW90ZUFkZHIiOiIxMjcuMC4wLjEifX19.eZ04WtIriWpOTolGAKUfAUrrxW0Vak1jnvsJEjAFz9I"
+ "eyJhbGciOiJIUzI1NiJ9.eyJtZXJjdXJlIjp7InB1Ymxpc2giOlsiKiJdLCJzdWJzY3JpYmUiOlsiaHR0cHM6Ly9leGFtcGxlLmNvbS9teS1wcml2YXRlLXRvcGljIiwie3NjaGVtZX06Ly97K2hvc3R9L2RlbW8vYm9va3Mve2lkfS5qc29ubGQiLCIvLndlbGwta25vd24vbWVyY3VyZS9zdWJzY3JpcHRpb25zey90b3BpY317L3N1YnNjcmliZXJ9Il0sInBheWxvYWQiOnsidXNlciI6Imh0dHBzOi8vZXhhbXBsZS5jb20vdXNlcnMvZHVuZ2xhcyIsInJlbW90ZUFkZHIiOiIxMjcuMC4wLjEifX19.KKPIikwUzRuB3DTpVw6ajzwSChwFw5omBMmMcWKiDcM"
)
- /** JWT to use to subscribe, fallbacks to JWT if not set and PRIVATE_UPDATES set */
+ /** JWT to use to subscribe, fallbacks to JWT if not set and PRIVATE_UPDATES
+ * set
+ */
val SubscriberJwt = Properties.envOrElse("SUBSCRIBER_JWT", null)
/** Number of concurrent subscribers initially connected */
@@ -65,13 +72,16 @@ class LoadTest extends Simulation {
val RandomConnectionDuration =
Properties.envOrElse("RANDOM_CONNECTION_DURATION", "true").toBoolean
- /** Send private updates with random topics instead of public ones always with the same topic */
+ /** Send private updates with random topics instead of public ones always with
+ * the same topic
+ */
var PrivateUpdates =
Properties.envOrElse("PRIVATE_UPDATES", "false").toBoolean
val rnd = new scala.util.Random
- /** Subscriber test as a function to handle conditional Authorization header */
+ /** Subscriber test as a function to handle conditional Authorization header
+ */
def subscriberTest() = {
var topic = "https://example.com"
if (PrivateUpdates) {
diff --git a/go.mod b/go.mod
index 16e0883c..0fb5cb05 100644
--- a/go.mod
+++ b/go.mod
@@ -1,42 +1,71 @@
module github.com/dunglas/mercure
-go 1.16
+go 1.21
+
+toolchain go1.22.0
+
+retract (
+ v0.14.7 // CI problem
+ v0.14.6 // Overwriten tag
+)
require (
- github.com/dgraph-io/ristretto v0.1.0
- github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 // indirect
- github.com/felixge/httpsnoop v1.0.2 // indirect
- github.com/fsnotify/fsnotify v1.5.1 // indirect
- github.com/gofrs/uuid v4.0.0+incompatible
- github.com/golang-jwt/jwt/v4 v4.0.1-0.20210824015611-2bd8ee77fc3a
- github.com/golang/glog v1.0.0 // indirect
- github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00 // indirect
- github.com/gorilla/handlers v1.5.1
- github.com/gorilla/mux v1.8.0
- github.com/go-redis/redis/v8 v8.11.1
- github.com/hashicorp/golang-lru v0.5.4
+ github.com/dgraph-io/ristretto v0.1.1
+ github.com/gofrs/uuid v4.4.0+incompatible
+ github.com/golang-jwt/jwt/v5 v5.2.1
+ github.com/gorilla/handlers v1.5.2
+ github.com/gorilla/mux v1.8.1
+ github.com/hashicorp/golang-lru v1.0.2
github.com/kevburnsjr/skipfilter v0.0.1
- github.com/kr/text v0.2.0 // indirect
- github.com/prometheus/client_golang v1.11.0
- github.com/prometheus/client_model v0.2.0
- github.com/prometheus/common v0.30.0 // indirect
- github.com/prometheus/procfs v0.7.3 // indirect
- github.com/smartystreets/assertions v1.2.0 // indirect
- github.com/spf13/cast v1.4.1 // indirect
- github.com/spf13/cobra v1.1.1
+ github.com/prometheus/client_golang v1.19.0
+ github.com/prometheus/client_model v0.6.1
+ github.com/redis/go-redis/v9 v9.5.1
+ github.com/spf13/cobra v1.6.1
github.com/spf13/pflag v1.0.5
- github.com/spf13/viper v1.8.1
- github.com/stretchr/testify v1.7.0
- github.com/unrolled/secure v1.0.9
- github.com/yosida95/uritemplate/v3 v3.0.1
- go.etcd.io/bbolt v1.3.6
- go.uber.org/atomic v1.9.0 // indirect
- go.uber.org/multierr v1.7.0 // indirect
- go.uber.org/zap v1.19.0
- golang.org/x/crypto v0.0.0-20210817164053-32db794688a5
- golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d // indirect
- golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf // indirect
- golang.org/x/text v0.3.7 // indirect
- google.golang.org/protobuf v1.27.1 // indirect
- gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
+ github.com/spf13/viper v1.18.2
+ github.com/stretchr/testify v1.9.0
+ github.com/unrolled/secure v1.14.0
+ github.com/yosida95/uritemplate/v3 v3.0.2
+ go.etcd.io/bbolt v1.3.9
+ go.uber.org/zap v1.27.0
+ golang.org/x/crypto v0.22.0
+)
+
+require (
+ github.com/MauriceGit/skiplist v0.0.0-20211105230623-77f5c8d3e145 // indirect
+ github.com/RoaringBitmap/roaring v1.9.1 // indirect
+ github.com/beorn7/perks v1.0.1 // indirect
+ github.com/bits-and-blooms/bitset v1.13.0 // indirect
+ github.com/cespare/xxhash/v2 v2.3.0 // indirect
+ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
+ github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 // indirect
+ github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
+ github.com/dustin/go-humanize v1.0.1 // indirect
+ github.com/felixge/httpsnoop v1.0.4 // indirect
+ github.com/fsnotify/fsnotify v1.7.0 // indirect
+ github.com/golang/glog v1.2.1 // indirect
+ github.com/hashicorp/hcl v1.0.0 // indirect
+ github.com/inconshreveable/mousetrap v1.0.1 // indirect
+ github.com/magiconair/properties v1.8.7 // indirect
+ github.com/mitchellh/mapstructure v1.5.0 // indirect
+ github.com/mschoch/smat v0.2.0 // indirect
+ github.com/pelletier/go-toml/v2 v2.2.0 // indirect
+ github.com/pkg/errors v0.9.1 // indirect
+ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
+ github.com/prometheus/common v0.52.2 // indirect
+ github.com/prometheus/procfs v0.13.0 // indirect
+ github.com/sagikazarmark/locafero v0.4.0 // indirect
+ github.com/sagikazarmark/slog-shim v0.1.0 // indirect
+ github.com/sourcegraph/conc v0.3.0 // indirect
+ github.com/spf13/afero v1.11.0 // indirect
+ github.com/spf13/cast v1.6.0 // indirect
+ github.com/subosito/gotenv v1.6.0 // indirect
+ go.uber.org/multierr v1.11.0 // indirect
+ golang.org/x/exp v0.0.0-20240404231335-c0f41cb1a7a0 // indirect
+ golang.org/x/net v0.24.0 // indirect
+ golang.org/x/sys v0.19.0 // indirect
+ golang.org/x/text v0.14.0 // indirect
+ google.golang.org/protobuf v1.33.0 // indirect
+ gopkg.in/ini.v1 v1.67.0 // indirect
+ gopkg.in/yaml.v3 v3.0.1 // indirect
)
diff --git a/go.sum b/go.sum
index d45ae575..d88c2aba 100644
--- a/go.sum
+++ b/go.sum
@@ -1,819 +1,176 @@
-cloud.google.com/go v0.26.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.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
-cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
-cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
-cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
-cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
-cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
-cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
-cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
-cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
-cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
-cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
-cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
-cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
-cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI=
-cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk=
-cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg=
-cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8=
-cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0=
-cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
-cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
-cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
-cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
-cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
-cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
-cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
-cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
-cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk=
-cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
-cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
-cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
-cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
-cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
-cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
-cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
-cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
-cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
-dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
-github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
-github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
-github.com/MauriceGit/skiplist v0.0.0-20191117202105-643e379adb62 h1:gDG99+yxKg62ymqQVT33Vb2oDaxTx5j3M87VdR8xcOs=
+cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA=
github.com/MauriceGit/skiplist v0.0.0-20191117202105-643e379adb62/go.mod h1:877WBceefKn14QwVVn4xRFUsHsZb9clICgdeTj4XsUg=
-github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
-github.com/RoaringBitmap/roaring v0.9.4 h1:ckvZSX5gwCRaJYBNe7syNawCU5oruY9gQmjXlp4riwo=
+github.com/MauriceGit/skiplist v0.0.0-20211105230623-77f5c8d3e145 h1:1yw6O62BReQ+uA1oyk9XaQTvLhcoHWmoQAgXmDFXpIY=
+github.com/MauriceGit/skiplist v0.0.0-20211105230623-77f5c8d3e145/go.mod h1:877WBceefKn14QwVVn4xRFUsHsZb9clICgdeTj4XsUg=
github.com/RoaringBitmap/roaring v0.9.4/go.mod h1:icnadbWcNyfEHlYdr+tDlOTih1Bf/h+rzPpv4sbomAA=
-github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
-github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
-github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
-github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
-github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
-github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
-github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
-github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
-github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
-github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
-github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
-github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
-github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
+github.com/RoaringBitmap/roaring v1.9.1 h1:LXcSqGGGMKm+KAzUyWn7ZeREqoOkoMX+KwLOK1thc4I=
+github.com/RoaringBitmap/roaring v1.9.1/go.mod h1:6AXUsoIEzDTFFQCe1RbGA6uFONMhvejWj5rqITANK90=
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/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
-github.com/bits-and-blooms/bitset v1.2.0 h1:Kn4yilvwNtMACtf1eYDlG8H77R07mZSPbMjLyS07ChA=
github.com/bits-and-blooms/bitset v1.2.0/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edYb8uY+O0FJTyyDA=
-github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84=
-github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM=
-github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
-github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
-github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
-github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY=
+github.com/bits-and-blooms/bitset v1.12.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
+github.com/bits-and-blooms/bitset v1.13.0 h1:bAQ9OPNFYbGHV6Nez0tmNI0RiEu7/hxlYJRUA0wFAVE=
+github.com/bits-and-blooms/bitset v1.13.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
-github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
-github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
-github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
-github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
-github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
-github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
-github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
-github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
-github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
+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/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
-github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
-github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
-github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
-github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
+github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/dgraph-io/ristretto v0.1.0 h1:Jv3CGQHp9OjuMBSne1485aDpUkTKEcUqF+jm/LuerPI=
-github.com/dgraph-io/ristretto v0.1.0/go.mod h1:fux0lOrBhrVCJd3lcTHsIJhq1T2rokOu6v9Vcb3Q9ug=
-github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
+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/dgraph-io/ristretto v0.1.1 h1:6CWw5tJNgpegArSHpNHJKldNeq03FQCwYvfMVWajOK8=
+github.com/dgraph-io/ristretto v0.1.1/go.mod h1:S1GPSBCYCIhmVNfcth17y2zZtQT6wzkzgwUve0VDWWA=
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 h1:fAjc9m62+UWV/WAFKLNi6ZS0675eEUC9y3AlwSbQu1Y=
github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
-github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
-github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
-github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
-github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
-github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
-github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
-github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
-github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
-github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
-github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
-github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
-github.com/felixge/httpsnoop v1.0.2 h1:+nS9g82KMXccJ/wp0zyRW9ZBHFETmMGtkk+2CTTrW4o=
-github.com/felixge/httpsnoop v1.0.2/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
-github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
-github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
-github.com/fsnotify/fsnotify v1.5.0 h1:NO5hkcB+srp1x6QmwvNZLeaOgbM8cmBTN32THzjvu2k=
-github.com/fsnotify/fsnotify v1.5.0/go.mod h1:BX0DCEr5pT4jm2CnQdVP1lFV521fcCNcyEeNp4DQQDk=
-github.com/fsnotify/fsnotify v1.5.1 h1:mZcQUHVQUQWoPXXtuf9yuEXKudkV2sx1E06UadKWpgI=
-github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU=
-github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
-github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
-github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
-github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
-github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
-github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
-github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
-github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
-github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
-github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
-github.com/go-redis/redis/v8 v8.11.1 h1:Aqf/1y2eVfE9zrySM++/efzwv3mkLH7n/T96//gbo94=
-github.com/go-redis/redis/v8 v8.11.1/go.mod h1:DLomh7y2e3ggQXQLd1YgmvIfecPJoFl7WU5SOQ/r06M=
-github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
-github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
-github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw=
-github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
-github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
-github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
+github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
+github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
+github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
+github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
+github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
+github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
+github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
+github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
+github.com/go-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0=
+github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA=
+github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
-github.com/golang-jwt/jwt/v4 v4.0.1-0.20210824015611-2bd8ee77fc3a h1:PkXyoVlXSgfo6UaTddRi4S72pr8A75+F1+mSNBtumEo=
-github.com/golang-jwt/jwt/v4 v4.0.1-0.20210824015611-2bd8ee77fc3a/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
+github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
+github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
-github.com/golang/glog v1.0.0 h1:nfP3RFugxnNRyKgeWd4oI1nYvXpxrx8ck8ZrcizshdQ=
-github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4=
-github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
-github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
-github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
-github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
-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/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
-github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
-github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
-github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
-github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
-github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8=
-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.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
-github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
-github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
-github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
-github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
-github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
-github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
-github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
-github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
-github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
-github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
-github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
-github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
-github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM=
-github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
-github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
-github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
-github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
-github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
-github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
-github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
-github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
-github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
-github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
-github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
-github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
-github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
-github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
-github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
-github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
-github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
-github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
-github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
-github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
-github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
-github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
-github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
-github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
-github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
-github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
-github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
-github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
-github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00 h1:l5lAOZEym3oK3SQ2HBHWsJUfbNBiTXJDeW2QDxw9AQ0=
-github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
-github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4=
-github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q=
-github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
-github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
-github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
-github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
-github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
-github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
-github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
-github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q=
-github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
-github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
-github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
-github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
-github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
-github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
-github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=
-github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
-github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
-github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
-github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
-github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
-github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
-github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
-github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc=
+github.com/golang/glog v1.2.1 h1:OptwRhECazUx5ix5TTWC3EZhsZEHWcYWY4FQHTIubm4=
+github.com/golang/glog v1.2.1/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w=
+github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
+github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
+github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+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/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=
github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
+github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c=
+github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
-github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
-github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
-github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
-github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
-github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
-github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
-github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
-github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
-github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
-github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
+github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc=
+github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
-github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
-github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
-github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
-github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
-github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
-github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
-github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
-github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
github.com/kevburnsjr/skipfilter v0.0.1 h1:EWl1lWUJfIehrKYIEkps0Cl67lCfS2pUM9iZFNajp7g=
github.com/kevburnsjr/skipfilter v0.0.1/go.mod h1:jfaRyFOYVUtIa6IIC+0mB1qiZqhHw+DKvFowCBuijSk=
-github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
-github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
-github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
-github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
-github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
-github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
-github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
-github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
-github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
-github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
-github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
+github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
-github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
-github.com/magiconair/properties v1.8.5 h1:b6kJs+EmPFMYGkow9GiUyCyOvIwYetYJ3fSaWak/Gls=
-github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
-github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
-github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
-github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
-github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
-github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
-github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
-github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
+github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
+github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
+github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
-github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
-github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
-github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
-github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
-github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
-github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag=
-github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
-github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
+github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
-github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
-github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/mschoch/smat v0.2.0 h1:8imxQsjDm8yFEAVBe7azKmKSgzSkZXDuKkSq9374khM=
github.com/mschoch/smat v0.2.0/go.mod h1:kc9mz7DoBKqDyiRL7VZN8KvXQMWeTaVnttLRXOlotKw=
-github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
-github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
-github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
-github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
-github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
-github.com/onsi/ginkgo v1.15.0/go.mod h1:hF8qUzuuC8DJGygJH3726JnCZX4MYbRB8yFfISqnKUg=
-github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
-github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
-github.com/onsi/gomega v1.10.5/go.mod h1:gza4q3jKQJijlu05nKWRCW/GavJumGt8aNRxWg7mt48=
-github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
-github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
-github.com/pelletier/go-toml v1.9.3 h1:zeC5b1GviRUyKYd6OJPvBU/mcVDVoL1OhT17FCt5dSQ=
-github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
-github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
-github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
+github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
+github.com/pelletier/go-toml/v2 v2.2.0 h1:QLgLl2yMN7N+ruc31VynXs1vhMZa7CeHHejIeBAsoHo=
+github.com/pelletier/go-toml/v2 v2.2.0/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
-github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI=
-github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
-github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
-github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
-github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
-github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
-github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
-github.com/prometheus/client_golang v1.11.0 h1:HNkLOAEQMIDv/K+04rukrLx6ch7msSRwf3/SASFAGtQ=
-github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
-github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
-github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
-github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
-github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M=
-github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
-github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
-github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
-github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
-github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
-github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc=
-github.com/prometheus/common v0.30.0 h1:JEkYlQnpzrzQFxi6gnukFPdQ+ac82oRhzMcIduJu/Ug=
-github.com/prometheus/common v0.30.0/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls=
-github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
-github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
-github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
-github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
-github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
-github.com/prometheus/procfs v0.7.3 h1:4jVXhlkAyzOScmCkXBTOLRLTz8EeU+eyjrwB/EPq0VU=
-github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
-github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
-github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
-github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
-github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
-github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
-github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
-github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
-github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
-github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
-github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
-github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
-github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
-github.com/smartystreets/assertions v1.2.0 h1:42S6lae5dvLc7BrLu/0ugRtcFVjoJNMC/N3yZFZkDFs=
-github.com/smartystreets/assertions v1.2.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo=
-github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
-github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
-github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
-github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
-github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
-github.com/spf13/afero v1.6.0 h1:xoax2sJ2DT8S8xA2paPFjDCScCNeWsg75VG0DLRreiY=
-github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I=
-github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
-github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
-github.com/spf13/cast v1.4.1 h1:s0hze+J0196ZfEMTs80N7UlFt0BDuQ7Q+JDnHiMWKdA=
-github.com/spf13/cast v1.4.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
-github.com/spf13/cobra v1.1.1 h1:KfztREH0tPxJJ+geloSLaAkaPkr4ki2Er5quFV1TDo4=
-github.com/spf13/cobra v1.1.1/go.mod h1:WnodtKOvamDL/PwE2M4iKs8aMDBZ5Q5klgD3qfVJQMI=
-github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
-github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
-github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
-github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
+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 v1.19.0 h1:ygXvpU1AoN1MhdzckN+PyD9QJOSD4x7kmXYlnfbA6JU=
+github.com/prometheus/client_golang v1.19.0/go.mod h1:ZRM9uEAypZakd+q/x7+gmsvXdURP+DABIEIjnmDdp+k=
+github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
+github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
+github.com/prometheus/common v0.52.2 h1:LW8Vk7BccEdONfrJBDffQGRtpSzi5CQaRZGtboOO2ck=
+github.com/prometheus/common v0.52.2/go.mod h1:lrWtQx+iDfn2mbH5GUzlH9TSHyfZpHkSiG1W7y3sF2Q=
+github.com/prometheus/procfs v0.13.0 h1:GqzLlQyfsPbaEHaQkO7tbDlriv/4o5Hudv6OXHGKX7o=
+github.com/prometheus/procfs v0.13.0/go.mod h1:cd4PFCR54QLnGKPaKGA6l+cfuNXtht43ZKY6tow0Y1g=
+github.com/redis/go-redis/v9 v9.5.1 h1:H1X4D3yHPaYrkL5X06Wh6xNVM/pX0Ft4RV0vMGvLBh8=
+github.com/redis/go-redis/v9 v9.5.1/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M=
+github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
+github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
+github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
+github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ=
+github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
+github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
+github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
+github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
+github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
+github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
+github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
+github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
+github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
+github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA=
+github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
-github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=
-github.com/spf13/viper v1.8.1 h1:Kq1fyeebqsBfbjZj4EL7gj2IO0mMaiyjYUWcUsl2O44=
-github.com/spf13/viper v1.8.1/go.mod h1:o0Pch8wJ9BVSWGQMbra6iw0oQ5oktSIBaujf1rJH9Ns=
+github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ=
+github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
-github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
-github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
-github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
+github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
+github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
-github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
-github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
-github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
-github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
-github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
-github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
-github.com/unrolled/secure v1.0.9 h1:BWRuEb1vDrBFFDdbCnKkof3gZ35I/bnHGyt0LB0TNyQ=
-github.com/unrolled/secure v1.0.9/go.mod h1:fO+mEan+FLB0CdEnHf6Q4ZZVNqG+5fuLFnP8p0BXDPI=
-github.com/urfave/negroni v1.0.0 h1:kIimOitoypq34K7TG7DUaJ9kq/N4Ofuwi1sjz0KipXc=
-github.com/urfave/negroni v1.0.0/go.mod h1:Meg73S6kFm/4PpbYdq35yYWoCZ9mS/YSx+lKnmiohz4=
-github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
-github.com/yosida95/uritemplate/v3 v3.0.1 h1:+Fs//CsT+x231WmUQhMHWMxZizMvpnkOVWop02mVCfs=
-github.com/yosida95/uritemplate/v3 v3.0.1/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
-github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
-github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
-github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
-github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
-github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
-go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
-go.etcd.io/bbolt v1.3.6 h1:/ecaJf0sk1l4l6V4awd65v2C3ILy7MSj+s/x1ADCIMU=
-go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4=
-go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs=
-go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g=
-go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ=
-go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
-go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
-go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
-go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
-go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
-go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
-go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
-go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
-go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
-go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
+github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
+github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
+github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
+github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
+github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
+github.com/unrolled/secure v1.14.0 h1:u9vJTU/pR4Bny0ntLUMxdfLtmIRGvQf2sEFuA0TG9AE=
+github.com/unrolled/secure v1.14.0/go.mod h1:BmF5hyM6tXczk3MpQkFf1hpKSRqCyhqcbiQtiAF7+40=
+github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
+github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
+go.etcd.io/bbolt v1.3.9 h1:8x7aARPEXiXbHmtUwAIv7eV2fQFHrLLavdiJ3uzJXoI=
+go.etcd.io/bbolt v1.3.9/go.mod h1:zaO32+Ti0PK1ivdPtgMESzuzL2VPoIG1PCQNvOdo/dE=
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
-go.uber.org/goleak v1.1.10 h1:z+mqJhf6ss6BSfSM671tgKyZBFPTTJM+HLxnhPC3wu0=
-go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A=
-go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
-go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
-go.uber.org/multierr v1.7.0 h1:zaiO/rmgFjbmCXdSYJWQcdvOCsthmdaHfr3Gm2Kx4Ec=
-go.uber.org/multierr v1.7.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak=
-go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
-go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo=
-go.uber.org/zap v1.19.0 h1:mZQZefskPPCMIBCSEH0v2/iUqqLrYtaeqwD6FUGUnFE=
-go.uber.org/zap v1.19.0/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI=
-golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
-golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/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-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
-golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
-golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
-golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
-golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
-golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 h1:HWj/xjIHfjYU5nVXpTM0s39J9CbLn7Cc5a7IC5rwsMQ=
-golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
-golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
-golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
-golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
-golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
-golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
-golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
-golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
-golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
-golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
-golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
-golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
-golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
-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/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
-golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
-golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
-golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
-golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
-golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
-golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
-golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
-golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
-golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 h1:VLliZ0d+/avPrXXH+OakdXhpJuEoBZuwh1m2j7U6Iug=
-golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
-golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
-golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
-golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
-golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
-golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
-golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
-golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
-golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
-golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
-golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
-golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
-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-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20181220203305-927f97764cc3/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-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
-golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
-golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
-golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
-golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
-golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
-golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
-golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
-golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
-golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
-golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
-golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
-golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
-golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
-golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
-golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
-golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
-golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
-golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
-golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
-golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
-golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=
-golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
-golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
-golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d h1:LO7XpTYMwTqxjLcGWPijK3vRXg1aWdlNOVOHRq45d7c=
-golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
-golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
-golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
-golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
-golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
-golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
-golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
-golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
-golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
-golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
-golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
-golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
-golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
-golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
-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-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/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-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/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-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf h1:2ucpDCmfkl8Bd/FsLtiD653Wf96cW37s+iGx93zsu4k=
-golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
-golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
-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.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
-golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
-golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
-golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
-golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
-golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
-golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/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-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-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
-golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
-golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
-golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
-golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
-golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
-golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
-golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
-golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
-golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
-golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
-golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
-golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
-golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
-golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
-golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
-golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
-golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
-golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
-golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
-golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
-golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
-golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
-golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
-golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
-golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
-golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
-golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
-golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
-golang.org/x/tools v0.1.2 h1:kRBLX7v7Af8W7Gdbbc908OJcdgtK8bOz9Uaj8/F1ACA=
-golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
-golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
-golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
-google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
-google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
-google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
-google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
-google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
-google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
-google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
-google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
-google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
-google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
-google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
-google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
-google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
-google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
-google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
-google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=
-google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE=
-google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=
-google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU=
-google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94=
-google.golang.org/api v0.44.0/go.mod h1:EBOGZqzyhtvMDoxwS97ctnh0zUmYY6CxqXsc1AvkYD8=
-google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
-google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
-google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
-google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
-google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
-google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
+go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
+go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
+go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
+go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
+go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
+go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
+golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30=
+golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
+golang.org/x/exp v0.0.0-20240404231335-c0f41cb1a7a0 h1:985EYyeCOxTpcgOTJpflJUwOeEz0CQOdPt73OzpE9F8=
+golang.org/x/exp v0.0.0-20240404231335-c0f41cb1a7a0/go.mod h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI=
+golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
+golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w=
+golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
+golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
+golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+golang.org/x/sys v0.0.0-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
+golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk=
+golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
+golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
+golang.org/x/tools v0.20.0/go.mod h1:WvitBU7JJf6A4jOdg4S1tviW9bhUxkgeCui/0JHctQg=
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
-google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
-google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
-google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
-google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
-google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
-google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
-google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
-google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
-google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
-google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
-google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
-google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
-google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
-google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
-google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
-google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
-google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
-google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
-google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
-google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
-google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
-google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
-google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
-google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
-google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
-google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
-google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
-google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A=
-google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
-google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
-google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
-google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
-google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
-google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
-google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
-google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
-google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
-google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
-google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
-google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
-google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
-google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
-google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
-google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
-google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=
-google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
-google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
-google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
-google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
-google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
-google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
-google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
-google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
-google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
-google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
-google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
-google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
-google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
-google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
-google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
-google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
-google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ=
-google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
-gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
+google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
+google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/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/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
-gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
-gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
-gopkg.in/ini.v1 v1.62.0 h1:duBzk771uxoUuOlyRLkHsygud9+5lrlGjdFBb4mSKDU=
-gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
-gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
+gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
+gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
-gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
-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.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
-gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-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=
-honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
-honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
-honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
-honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
-honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
-rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
-rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
-rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/handler.go b/handler.go
index 26451adb..03d6b9ab 100644
--- a/handler.go
+++ b/handler.go
@@ -29,7 +29,7 @@ func (h *Hub) initHandler() {
csp := "default-src 'self'"
if h.demo {
- router.PathPrefix(defaultDemoURL).HandlerFunc(h.Demo).Methods("GET", "HEAD")
+ router.PathPrefix(defaultDemoURL).HandlerFunc(h.Demo).Methods(http.MethodGet, http.MethodHead)
}
if h.ui {
public, err := fs.Sub(uiContent, "public")
@@ -44,8 +44,8 @@ func (h *Hub) initHandler() {
h.registerSubscriptionHandlers(router)
- router.HandleFunc(defaultHubURL, h.SubscribeHandler).Methods("GET", "HEAD")
- router.HandleFunc(defaultHubURL, h.PublishHandler).Methods("POST")
+ router.HandleFunc(defaultHubURL, h.SubscribeHandler).Methods(http.MethodGet, http.MethodHead)
+ router.HandleFunc(defaultHubURL, h.PublishHandler).Methods(http.MethodPost)
secureMiddleware := secure.New(secure.Options{
IsDevelopment: h.debug,
@@ -82,18 +82,22 @@ func (h *Hub) Serve() { //nolint:funlen
addr := h.config.GetString("addr")
h.server = &http.Server{
- Addr: addr,
- Handler: h.baseHandler(),
- ReadTimeout: h.config.GetDuration("read_timeout"),
- WriteTimeout: h.config.GetDuration("write_timeout"),
+ Addr: addr,
+ Handler: h.baseHandler(),
+ ReadTimeout: h.config.GetDuration("read_timeout"),
+ ReadHeaderTimeout: h.config.GetDuration("read_header_timeout"),
+ WriteTimeout: h.config.GetDuration("write_timeout"),
}
if _, ok := h.metrics.(*PrometheusMetrics); ok {
addr := h.config.GetString("metrics_addr")
h.metricsServer = &http.Server{
- Addr: addr,
- Handler: h.metricsHandler(),
+ Addr: addr,
+ Handler: h.metricsHandler(),
+ ReadTimeout: h.config.GetDuration("read_timeout"),
+ ReadHeaderTimeout: h.config.GetDuration("read_header_timeout"),
+ WriteTimeout: h.config.GetDuration("write_timeout"),
}
if c := h.logger.Check(zap.InfoLevel, "Mercure metrics started"); c != nil {
@@ -131,7 +135,7 @@ func (h *Hub) Serve() { //nolint:funlen
h.server.TLSConfig = certManager.TLSConfig()
// Mandatory for Let's Encrypt http-01 challenge
- go http.ListenAndServe(h.config.GetString("acme_http01_addr"), certManager.HTTPHandler(nil))
+ go http.ListenAndServe(h.config.GetString("acme_http01_addr"), certManager.HTTPHandler(nil)) //nolint:gosec
}
if c := h.logger.Check(zap.InfoLevel, "Mercure started"); c != nil {
@@ -172,9 +176,11 @@ func (h *Hub) listenShutdown() <-chan struct{} {
c.Write(zap.Error(err))
}
}
- if err := h.metricsServer.Shutdown(context.Background()); err != nil {
- if c := h.logger.Check(zap.ErrorLevel, "Unexpected error during metrics server shutdown"); c != nil {
- c.Write(zap.Error(err))
+ if h.metricsServer != nil {
+ if err := h.metricsServer.Shutdown(context.Background()); err != nil {
+ if c := h.logger.Check(zap.ErrorLevel, "Unexpected error during metrics server shutdown"); c != nil {
+ c.Write(zap.Error(err))
+ }
}
}
if c := h.logger.Check(zap.InfoLevel, "My Baby Shot Me Down"); c != nil {
@@ -196,12 +202,12 @@ func (h *Hub) chainHandlers() http.Handler { //nolint:funlen
r := mux.NewRouter()
h.registerSubscriptionHandlers(r)
- r.HandleFunc(defaultHubURL, h.SubscribeHandler).Methods("GET", "HEAD")
- r.HandleFunc(defaultHubURL, h.PublishHandler).Methods("POST")
+ r.HandleFunc(defaultHubURL, h.SubscribeHandler).Methods(http.MethodGet, http.MethodHead)
+ r.HandleFunc(defaultHubURL, h.PublishHandler).Methods(http.MethodPost)
csp := "default-src 'self'"
if h.demo {
- r.PathPrefix("/demo").HandlerFunc(h.Demo).Methods("GET", "HEAD")
+ r.PathPrefix("/demo").HandlerFunc(h.Demo).Methods(http.MethodGet, http.MethodHead)
}
if h.ui {
@@ -213,7 +219,7 @@ func (h *Hub) chainHandlers() http.Handler { //nolint:funlen
r.PathPrefix("/").Handler(http.FileServer(http.FS(public)))
csp += " mercure.rocks cdn.jsdelivr.net"
} else {
- r.HandleFunc("/", welcomeHandler).Methods("GET", "HEAD")
+ r.HandleFunc("/", welcomeHandler).Methods(http.MethodGet, http.MethodHead)
}
secureMiddleware := secure.New(secure.Options{
@@ -250,7 +256,13 @@ func (h *Hub) chainHandlers() http.Handler { //nolint:funlen
}
secureHandler := secureMiddleware.Handler(useForwardedHeadersHandlers)
- loggingHandler := handlers.CombinedLoggingHandler(os.Stderr, secureHandler)
+
+ var loggingHandler http.Handler
+ if h.logger != nil && h.logger.Level().Enabled(zap.FatalLevel) {
+ loggingHandler = handlers.CombinedLoggingHandler(os.Stderr, secureHandler)
+ } else {
+ loggingHandler = secureHandler
+ }
recoveryHandler := handlers.RecoveryHandler(
handlers.RecoveryLogger(zapRecoveryHandlerLogger{h.logger}),
handlers.PrintRecoveryStack(h.debug),
@@ -274,9 +286,9 @@ func (h *Hub) registerSubscriptionHandlers(r *mux.Router) {
r.UseEncodedPath()
r.SkipClean(true)
- r.HandleFunc(subscriptionURL, h.SubscriptionHandler).Methods("GET")
- r.HandleFunc(subscriptionsForTopicURL, h.SubscriptionsHandler).Methods("GET")
- r.HandleFunc(subscriptionsURL, h.SubscriptionsHandler).Methods("GET")
+ r.HandleFunc(subscriptionURL, h.SubscriptionHandler).Methods(http.MethodGet)
+ r.HandleFunc(subscriptionsForTopicURL, h.SubscriptionsHandler).Methods(http.MethodGet)
+ r.HandleFunc(subscriptionsURL, h.SubscriptionsHandler).Methods(http.MethodGet)
}
// Deprecated: use the Caddy server module or the standalone library instead.
@@ -306,13 +318,13 @@ func (h *Hub) metricsHandler() http.Handler {
// Deprecated: use the Caddy server module or the standalone library instead.
func registerHealthz(router *mux.Router) {
- router.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
+ router.HandleFunc("/healthz", func(w http.ResponseWriter, _ *http.Request) {
fmt.Fprint(w, "ok")
- }).Methods("GET", "HEAD")
+ }).Methods(http.MethodGet, http.MethodHead)
}
// Deprecated: use the Caddy server module or the standalone library instead.
-func welcomeHandler(w http.ResponseWriter, r *http.Request) {
+func welcomeHandler(w http.ResponseWriter, _ *http.Request) {
fmt.Fprint(w, `
Mercure Hub
`)
diff --git a/hub.go b/hub.go
index f3909070..cddbdb89 100644
--- a/hub.go
+++ b/hub.go
@@ -3,15 +3,26 @@
package mercure
import (
+ "errors"
"fmt"
"net/http"
+ "net/url"
"time"
- "github.com/golang-jwt/jwt/v4"
+ "github.com/golang-jwt/jwt/v5"
"github.com/spf13/viper"
"go.uber.org/zap"
)
+const (
+ DefaultWriteTimeout = 600 * time.Second
+ DefaultDispatchTimeout = 5 * time.Second
+ DefaultHeartbeat = 40 * time.Second
+)
+
+// ErrUnsupportedProtocolVersion is returned when the version passed is unsupported.
+var ErrUnsupportedProtocolVersion = errors.New("compatibility mode only supports protocol version 7")
+
// Option instances allow to configure the library.
type Option func(h *opt) error
@@ -105,37 +116,41 @@ func WithHeartbeat(interval time.Duration) Option {
}
}
-// WithPublisherJWT sets the JWT key and the signing algorithm to use for publishers.
-func WithPublisherJWT(key []byte, alg string) Option {
+// WithPublisherJWTKeyFunc sets the function to use to parse and verify the publisher JWT.
+func WithPublisherJWTKeyFunc(keyfunc jwt.Keyfunc) Option {
return func(o *opt) error {
- sm := jwt.GetSigningMethod(alg)
- switch sm.(type) {
- case *jwt.SigningMethodHMAC:
- case *jwt.SigningMethodRSA:
- default:
- return ErrUnexpectedSigningMethod
- }
+ o.publisherJWTKeyFunc = keyfunc
+
+ return nil
+ }
+}
- o.publisherJWT = &jwtConfig{key, sm}
+// WithSubscriberJWTKeyFunc sets the function to use to parse and verify the subscriber JWT.
+func WithSubscriberJWTKeyFunc(keyfunc jwt.Keyfunc) Option {
+ return func(o *opt) error {
+ o.subscriberJWTKeyFunc = keyfunc
return nil
}
}
+// WithPublisherJWT sets the JWT key and the signing algorithm to use for publishers.
+func WithPublisherJWT(key []byte, alg string) Option {
+ return func(o *opt) error {
+ keyfunc, err := createJWTKeyfunc(key, alg)
+ o.publisherJWTKeyFunc = keyfunc
+
+ return err
+ }
+}
+
// WithSubscriberJWT sets the JWT key and the signing algorithm to use for subscribers.
func WithSubscriberJWT(key []byte, alg string) Option {
return func(o *opt) error {
- sm := jwt.GetSigningMethod(alg)
- switch sm.(type) {
- case *jwt.SigningMethodHMAC:
- case *jwt.SigningMethodRSA:
- default:
- return ErrUnexpectedSigningMethod
- }
-
- o.subscriberJWT = &jwtConfig{key, sm}
+ keyfunc, err := createJWTKeyfunc(key, alg)
+ o.subscriberJWTKeyFunc = keyfunc
- return nil
+ return err
}
}
@@ -148,9 +163,35 @@ func WithAllowedHosts(hosts []string) Option {
}
}
+func validateOrigins(origins []string) error {
+ for _, origin := range origins {
+ switch origin {
+ case "*", "null":
+ continue
+ }
+
+ u, err := url.Parse(origin)
+ if err != nil ||
+ !u.IsAbs() ||
+ u.Opaque != "" ||
+ u.User != nil ||
+ u.Path != "" ||
+ u.RawQuery != "" ||
+ u.Fragment != "" {
+ return fmt.Errorf(`invalid origin, must be a URL having only a scheme, a host and optionally a port, "*" or "null": %w`, err)
+ }
+ }
+
+ return nil
+}
+
// WithPublishOrigins sets the origins allowed to publish updates.
func WithPublishOrigins(origins []string) Option {
return func(o *opt) error {
+ if err := validateOrigins(origins); err != nil {
+ return err
+ }
+
o.publishOrigins = origins
return nil
@@ -160,6 +201,10 @@ func WithPublishOrigins(origins []string) Option {
// WithCORSOrigins sets the allowed CORS origins.
func WithCORSOrigins(origins []string) Option {
return func(o *opt) error {
+ if err := validateOrigins(origins); err != nil {
+ return err
+ }
+
o.corsOrigins = origins
return nil
@@ -193,33 +238,47 @@ func WithCookieName(cookieName string) Option {
}
}
-type jwtConfig struct {
- key []byte
- signingMethod jwt.SigningMethod
+// WithProtocolVersionCompatibility sets the version of the Mercure protocol to be backward compatible with (only version 7 is supported).
+func WithProtocolVersionCompatibility(protocolVersionCompatibility int) Option {
+ return func(o *opt) error {
+ switch protocolVersionCompatibility {
+ case 7:
+ o.protocolVersionCompatibility = protocolVersionCompatibility
+
+ return nil
+ default:
+ return ErrUnsupportedProtocolVersion
+ }
+ }
}
// opt contains the available options.
//
// If you change this, also update the Caddy module and the documentation.
type opt struct {
- transport Transport
- topicSelectorStore *TopicSelectorStore
- anonymous bool
- debug bool
- subscriptions bool
- ui bool
- demo bool
- logger Logger
- writeTimeout time.Duration
- dispatchTimeout time.Duration
- heartbeat time.Duration
- publisherJWT *jwtConfig
- subscriberJWT *jwtConfig
- metrics Metrics
- allowedHosts []string
- publishOrigins []string
- corsOrigins []string
- cookieName string
+ transport Transport
+ topicSelectorStore *TopicSelectorStore
+ anonymous bool
+ debug bool
+ subscriptions bool
+ ui bool
+ demo bool
+ logger Logger
+ writeTimeout time.Duration
+ dispatchTimeout time.Duration
+ heartbeat time.Duration
+ publisherJWTKeyFunc jwt.Keyfunc
+ subscriberJWTKeyFunc jwt.Keyfunc
+ metrics Metrics
+ allowedHosts []string
+ publishOrigins []string
+ corsOrigins []string
+ cookieName string
+ protocolVersionCompatibility int
+}
+
+func (o *opt) isBackwardCompatiblyEnabledWith(version int) bool {
+ return o.protocolVersionCompatibility != 0 && version >= o.protocolVersionCompatibility
}
// Hub stores channels with clients currently subscribed and allows to dispatch updates.
@@ -235,7 +294,11 @@ type Hub struct {
// NewHub creates a new Hub instance.
func NewHub(options ...Option) (*Hub, error) {
- opt := &opt{writeTimeout: 600 * time.Second}
+ opt := &opt{
+ writeTimeout: DefaultWriteTimeout,
+ dispatchTimeout: DefaultDispatchTimeout,
+ heartbeat: DefaultHeartbeat,
+ }
for _, o := range options {
if err := o(opt); err != nil {
@@ -262,7 +325,7 @@ func NewHub(options ...Option) (*Hub, error) {
}
if opt.transport == nil {
- t, _ := NewLocalTransport(nil, nil, nil)
+ t, _ := NewLocalTransport(nil, nil)
opt.transport = t
}
diff --git a/hub_test.go b/hub_test.go
index 68dcbc18..b54763b2 100644
--- a/hub_test.go
+++ b/hub_test.go
@@ -1,14 +1,16 @@
package mercure
import (
- "errors"
+ "fmt"
+ "net/http"
"net/http/httptest"
"os"
"os/exec"
"sync"
"testing"
+ "time"
- "github.com/golang-jwt/jwt/v4"
+ "github.com/golang-jwt/jwt/v5"
"github.com/spf13/viper"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -24,6 +26,12 @@ func TestNewHub(t *testing.T) {
h := createDummy()
assert.IsType(t, &viper.Viper{}, h.config)
+
+ assert.False(t, h.opt.anonymous)
+ assert.Equal(t, defaultCookieName, h.opt.cookieName)
+ assert.Equal(t, 40*time.Second, h.opt.heartbeat)
+ assert.Equal(t, 5*time.Second, h.opt.dispatchTimeout)
+ assert.Equal(t, 600*time.Second, h.opt.writeTimeout)
}
func TestNewHubWithConfig(t *testing.T) {
@@ -32,7 +40,7 @@ func TestNewHubWithConfig(t *testing.T) {
WithSubscriberJWT([]byte("bar"), jwt.SigningMethodHS256.Name),
)
require.NotNil(t, h)
- require.Nil(t, err)
+ require.NoError(t, err)
}
func TestNewHubValidationError(t *testing.T) {
@@ -63,7 +71,7 @@ func TestStartCrash(t *testing.T) {
err := cmd.Run()
var e *exec.ExitError
- require.True(t, errors.As(err, &e))
+ require.ErrorAs(t, err, &e)
assert.False(t, e.Success())
}
@@ -94,9 +102,9 @@ func TestStop(t *testing.T) {
for i := 0; i < numberOfSubscribers; i++ {
go func() {
defer wg.Done()
- req := httptest.NewRequest("GET", defaultHubURL+"?topic=http://example.com/foo", nil)
+ req := httptest.NewRequest(http.MethodGet, defaultHubURL+"?topic=http://example.com/foo", nil)
- w := httptest.NewRecorder()
+ w := newSubscribeRecorder()
hub.SubscribeHandler(w, req)
r := w.Result()
@@ -108,6 +116,123 @@ func TestStop(t *testing.T) {
wg.Wait()
}
+func TestWithProtocolVersionCompatibility(t *testing.T) {
+ op := &opt{}
+
+ assert.False(t, op.isBackwardCompatiblyEnabledWith(7))
+
+ o := WithProtocolVersionCompatibility(7)
+ require.NoError(t, o(op))
+ assert.Equal(t, 7, op.protocolVersionCompatibility)
+ assert.True(t, op.isBackwardCompatiblyEnabledWith(7))
+ assert.True(t, op.isBackwardCompatiblyEnabledWith(8))
+ assert.False(t, op.isBackwardCompatiblyEnabledWith(6))
+}
+
+func TestWithProtocolVersionCompatibilityVersions(t *testing.T) {
+ op := &opt{}
+
+ testCases := []struct {
+ version int
+ ok bool
+ }{
+ {5, false},
+ {6, false},
+ {7, true},
+ {8, false},
+ }
+
+ for _, tc := range testCases {
+ t.Run(fmt.Sprintf("version %d", tc.version), func(t *testing.T) {
+ o := WithProtocolVersionCompatibility(tc.version)
+
+ if tc.ok {
+ require.NoError(t, o(op))
+ } else {
+ require.Error(t, o(op))
+ }
+ })
+ }
+}
+
+func TestWithPublisherJWTKeyFunc(t *testing.T) {
+ op := &opt{}
+
+ o := WithPublisherJWTKeyFunc(func(_ *jwt.Token) (interface{}, error) { return []byte{}, nil })
+ require.NoError(t, o(op))
+ require.NotNil(t, op.publisherJWTKeyFunc)
+}
+
+func TestWithSubscriberJWTKeyFunc(t *testing.T) {
+ op := &opt{}
+
+ o := WithSubscriberJWTKeyFunc(func(_ *jwt.Token) (interface{}, error) { return []byte{}, nil })
+ require.NoError(t, o(op))
+ require.NotNil(t, op.subscriberJWTKeyFunc)
+}
+
+func TestWithDebug(t *testing.T) {
+ op := &opt{}
+
+ o := WithDebug()
+ require.NoError(t, o(op))
+ require.True(t, op.debug)
+}
+
+func TestWithUI(t *testing.T) {
+ op := &opt{}
+
+ o := WithUI()
+ require.NoError(t, o(op))
+ require.True(t, op.ui)
+}
+
+func TestOriginsValidator(t *testing.T) {
+ op := &opt{}
+
+ validOrigins := [][]string{
+ {"*"},
+ {"null"},
+ {"https://example.com"},
+ {"http://example.com:8000"},
+ {"https://example.com", "http://example.org"},
+ {"https://example.com", "*"},
+ {"null", "https://example.com:3000"},
+ {"capacitor://"},
+ {"capacitor://www.example.com"},
+ {"ionic://"},
+ {"foobar://"},
+ }
+
+ invalidOrigins := [][]string{
+ {"f"},
+ {"foo"},
+ {"https://example.com", "bar"},
+ {"https://example.com/"},
+ {"https://user@example.com"},
+ {"https://example.com:abc"},
+ {"https://example.com", "http://example.org/hello"},
+ {"https://example.com?query", "*"},
+ {"null", "https://example.com:3000#fragment"},
+ }
+
+ for _, origins := range validOrigins {
+ o := WithPublishOrigins(origins)
+ require.NoError(t, o(op), "error while not expected for %#v", origins)
+
+ o = WithCORSOrigins(origins)
+ require.NoError(t, o(op), "error while not expected for %#v", origins)
+ }
+
+ for _, origins := range invalidOrigins {
+ o := WithPublishOrigins(origins)
+ require.Error(t, o(op), "no error while expected for %#v", origins)
+
+ o = WithCORSOrigins(origins)
+ require.Error(t, o(op), "no error while expected for %#v", origins)
+ }
+}
+
func createDummy(options ...Option) *Hub {
tss, _ := NewTopicSelectorStoreLRU(0, 0)
options = append(
@@ -137,20 +262,22 @@ func createAnonymousDummy(options ...Option) *Hub {
return createDummy(options...)
}
-func createDummyAuthorizedJWT(h *Hub, r role, topics []string) string {
+func createDummyAuthorizedJWT(r role, topics []string) string {
+ return createDummyAuthorizedJWTWithPayload(r, topics, struct {
+ Foo string `json:"foo"`
+ }{Foo: "bar"})
+}
+
+func createDummyAuthorizedJWTWithPayload(r role, topics []string, payload interface{}) string {
token := jwt.New(jwt.SigningMethodHS256)
var key []byte
switch r {
case rolePublisher:
token.Claims = &claims{Mercure: mercureClaim{Publish: topics}, RegisteredClaims: jwt.RegisteredClaims{}}
- key = h.publisherJWT.key
+ key = []byte("publisher")
case roleSubscriber:
- var payload struct {
- Foo string `json:"foo"`
- }
- payload.Foo = "bar"
token.Claims = &claims{
Mercure: mercureClaim{
Subscribe: topics,
@@ -159,7 +286,7 @@ func createDummyAuthorizedJWT(h *Hub, r role, topics []string) string {
RegisteredClaims: jwt.RegisteredClaims{},
}
- key = h.subscriberJWT.key
+ key = []byte("subscriber")
}
tokenString, _ := token.SignedString(key)
diff --git a/jwt_keyfunc.go b/jwt_keyfunc.go
new file mode 100644
index 00000000..af2d8f0a
--- /dev/null
+++ b/jwt_keyfunc.go
@@ -0,0 +1,38 @@
+package mercure
+
+import (
+ "errors"
+ "fmt"
+
+ "github.com/golang-jwt/jwt/v5"
+)
+
+// ErrUnexpectedSigningMethod is returned when the signing JWT method is not supported.
+var ErrUnexpectedSigningMethod = errors.New("unexpected signing method")
+
+func createJWTKeyfunc(key []byte, alg string) (jwt.Keyfunc, error) {
+ signingMethod := jwt.GetSigningMethod(alg)
+
+ var k interface{}
+ switch signingMethod.(type) {
+ case *jwt.SigningMethodHMAC:
+ k = key
+ case *jwt.SigningMethodRSA:
+ pub, err := jwt.ParseRSAPublicKeyFromPEM(key)
+ if err != nil {
+ return nil, fmt.Errorf("unable to parse RSA public key: %w", err)
+ }
+
+ k = pub
+ default:
+ return nil, fmt.Errorf("%T: %w", signingMethod, ErrUnexpectedSigningMethod)
+ }
+
+ return func(t *jwt.Token) (interface{}, error) {
+ if t.Method != signingMethod {
+ return nil, fmt.Errorf("%T: %w", t.Method, ErrUnexpectedSigningMethod)
+ }
+
+ return k, nil
+ }, nil
+}
diff --git a/jwt_keyfunc_test.go b/jwt_keyfunc_test.go
new file mode 100644
index 00000000..044d43eb
--- /dev/null
+++ b/jwt_keyfunc_test.go
@@ -0,0 +1,25 @@
+package mercure
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/require"
+)
+
+func TestCreateJWTKeyfunc(t *testing.T) {
+ f, err := createJWTKeyfunc(([]byte{}), "invalid")
+ require.Error(t, err)
+ require.Nil(t, f)
+}
+
+func TestAuthorizeAuthorizationHeaderEmptyKeyRsa(t *testing.T) {
+ keyfunc, err := createJWTKeyfunc([]byte{}, "RS256")
+ require.EqualError(t, err, "unable to parse RSA public key: invalid key: Key must be a PEM encoded PKCS1 or PKCS8 key")
+ require.Nil(t, keyfunc)
+}
+
+func TestAuthorizeAuthorizationHeaderInvalidKeyRsa(t *testing.T) {
+ keyfunc, err := createJWTKeyfunc([]byte(privateKeyRsa), "RS256")
+ require.EqualError(t, err, "unable to parse RSA public key: asn1: structure error: integer too large")
+ require.Nil(t, keyfunc)
+}
diff --git a/local_transport.go b/local_transport.go
index 7100be76..f6a8d246 100644
--- a/local_transport.go
+++ b/local_transport.go
@@ -19,7 +19,7 @@ type LocalTransport struct {
}
// NewLocalTransport create a new LocalTransport.
-func NewLocalTransport(u *url.URL, l Logger, tss *TopicSelectorStore) (Transport, error) { //nolint:ireturn
+func NewLocalTransport(_ *url.URL, _ Logger) (Transport, error) { //nolint:ireturn
return &LocalTransport{
subscribers: NewSubscriberList(1e5),
closed: make(chan struct{}),
@@ -86,14 +86,7 @@ func (t *LocalTransport) GetSubscribers() (string, []*Subscriber, error) {
t.RLock()
defer t.RUnlock()
- var subscribers []*Subscriber
- t.subscribers.Walk(0, func(s *Subscriber) bool {
- subscribers = append(subscribers, s)
-
- return true
- })
-
- return t.lastEventID, subscribers, nil
+ return t.lastEventID, getSubscribers(t.subscribers), nil
}
// Close closes the Transport.
diff --git a/local_transport_bench_test.go b/local_transport_bench_test.go
index 23c80d9a..c6ddd2b1 100644
--- a/local_transport_bench_test.go
+++ b/local_transport_bench_test.go
@@ -18,7 +18,7 @@ func BenchmarkLocalTransport(b *testing.B) {
func subBenchLocalTransport(b *testing.B, topics, concurrency, matchPct int, testName string) {
b.Helper()
- tr, err := NewLocalTransport(&url.URL{Scheme: "local"}, zap.NewNop(), nil)
+ tr, err := NewLocalTransport(&url.URL{Scheme: "local"}, zap.NewNop())
if err != nil {
panic(err)
}
diff --git a/local_transport_test.go b/local_transport_test.go
index e9ccadab..15852bfa 100644
--- a/local_transport_test.go
+++ b/local_transport_test.go
@@ -11,17 +11,17 @@ import (
)
func TestLocalTransportDoNotDispatchUntilListen(t *testing.T) {
- transport, _ := NewLocalTransport(&url.URL{Scheme: "local"}, zap.NewNop(), nil)
+ transport, _ := NewLocalTransport(&url.URL{Scheme: "local"}, zap.NewNop())
defer transport.Close()
assert.Implements(t, (*Transport)(nil), transport)
u := &Update{Topics: []string{"http://example.com/books/1"}}
err := transport.Dispatch(u)
- require.Nil(t, err)
+ require.NoError(t, err)
s := NewSubscriber("", zap.NewNop())
s.SetTopics(u.Topics, nil)
- require.Nil(t, transport.AddSubscriber(s))
+ require.NoError(t, transport.AddSubscriber(s))
var wg sync.WaitGroup
wg.Add(1)
@@ -37,28 +37,27 @@ func TestLocalTransportDoNotDispatchUntilListen(t *testing.T) {
}
func TestLocalTransportDispatch(t *testing.T) {
- transport, _ := NewLocalTransport(&url.URL{Scheme: "local"}, zap.NewNop(), nil)
+ transport, _ := NewLocalTransport(&url.URL{Scheme: "local"}, zap.NewNop())
defer transport.Close()
assert.Implements(t, (*Transport)(nil), transport)
s := NewSubscriber("", zap.NewNop())
s.SetTopics([]string{"http://example.com/foo"}, nil)
- assert.Nil(t, transport.AddSubscriber(s))
+ require.NoError(t, transport.AddSubscriber(s))
- u := &Update{Topics: s.Topics}
- require.Nil(t, transport.Dispatch(u))
+ u := &Update{Topics: s.SubscribedTopics}
+ require.NoError(t, transport.Dispatch(u))
assert.Equal(t, u, <-s.Receive())
}
func TestLocalTransportClosed(t *testing.T) {
- transport, _ := NewLocalTransport(&url.URL{Scheme: "local"}, zap.NewNop(), nil)
+ transport, _ := NewLocalTransport(&url.URL{Scheme: "local"}, zap.NewNop())
defer transport.Close()
assert.Implements(t, (*Transport)(nil), transport)
s := NewSubscriber("", zap.NewNop())
- require.Nil(t, transport.AddSubscriber(s))
-
- assert.Nil(t, transport.Close())
+ require.NoError(t, transport.AddSubscriber(s))
+ require.NoError(t, transport.Close())
assert.Equal(t, transport.AddSubscriber(NewSubscriber("", zap.NewNop())), ErrClosedTransport)
assert.Equal(t, transport.Dispatch(&Update{}), ErrClosedTransport)
@@ -67,15 +66,15 @@ func TestLocalTransportClosed(t *testing.T) {
}
func TestLiveCleanDisconnectedSubscribers(t *testing.T) {
- tr, _ := NewLocalTransport(&url.URL{Scheme: "local"}, zap.NewNop(), nil)
+ tr, _ := NewLocalTransport(&url.URL{Scheme: "local"}, zap.NewNop())
transport := tr.(*LocalTransport)
defer transport.Close()
s1 := NewSubscriber("", zap.NewNop())
- require.Nil(t, transport.AddSubscriber(s1))
+ require.NoError(t, transport.AddSubscriber(s1))
s2 := NewSubscriber("", zap.NewNop())
- require.Nil(t, transport.AddSubscriber(s2))
+ require.NoError(t, transport.AddSubscriber(s2))
assert.Equal(t, 2, transport.subscribers.Len())
@@ -89,36 +88,36 @@ func TestLiveCleanDisconnectedSubscribers(t *testing.T) {
}
func TestLiveReading(t *testing.T) {
- transport, _ := NewLocalTransport(&url.URL{Scheme: "local"}, zap.NewNop(), nil)
+ transport, _ := NewLocalTransport(&url.URL{Scheme: "local"}, zap.NewNop())
defer transport.Close()
assert.Implements(t, (*Transport)(nil), transport)
s := NewSubscriber("", zap.NewNop())
s.SetTopics([]string{"https://example.com"}, nil)
- require.Nil(t, transport.AddSubscriber(s))
+ require.NoError(t, transport.AddSubscriber(s))
- u := &Update{Topics: s.Topics}
- assert.Nil(t, transport.Dispatch(u))
+ u := &Update{Topics: s.SubscribedTopics}
+ require.NoError(t, transport.Dispatch(u))
receivedUpdate := <-s.Receive()
assert.Equal(t, u, receivedUpdate)
}
func TestLocalTransportGetSubscribers(t *testing.T) {
- transport, _ := NewLocalTransport(&url.URL{Scheme: "local"}, zap.NewNop(), nil)
+ transport, _ := NewLocalTransport(&url.URL{Scheme: "local"}, zap.NewNop())
defer transport.Close()
require.NotNil(t, transport)
s1 := NewSubscriber("", zap.NewNop())
- require.Nil(t, transport.AddSubscriber(s1))
+ require.NoError(t, transport.AddSubscriber(s1))
s2 := NewSubscriber("", zap.NewNop())
- require.Nil(t, transport.AddSubscriber(s2))
+ require.NoError(t, transport.AddSubscriber(s2))
lastEventID, subscribers, err := transport.(TransportSubscribers).GetSubscribers()
+ require.NoError(t, err)
assert.Equal(t, EarliestLastEventID, lastEventID)
assert.Len(t, subscribers, 2)
assert.Contains(t, subscribers, s1)
assert.Contains(t, subscribers, s2)
- assert.Nil(t, err)
}
diff --git a/log.go b/log.go
index a0be8aa5..1e1c9efd 100644
--- a/log.go
+++ b/log.go
@@ -27,5 +27,6 @@ type CheckedEntry = zapcore.CheckedEntry
type Logger interface {
Info(msg string, fields ...LogField)
Error(msg string, fields ...LogField)
- Check(Level, string) *CheckedEntry
+ Check(level Level, msg string) *CheckedEntry
+ Level() Level
}
diff --git a/metrics.go b/metrics.go
index 7982a5fb..e73bc9d7 100644
--- a/metrics.go
+++ b/metrics.go
@@ -1,14 +1,18 @@
package mercure
import (
+ "net/http"
+
"github.com/dunglas/mercure/common"
"github.com/gorilla/mux"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
+const metricsPath = "/metrics"
+
type Metrics interface {
- // SubscriberConnected collects metrics about about subscriber connections.
+ // SubscriberConnected collects metrics about subscriber connections.
SubscriberConnected(s *Subscriber)
// SubscriberDisconnected collects metrics about subscriber disconnections.
SubscriberDisconnected(s *Subscriber)
@@ -18,9 +22,9 @@ type Metrics interface {
type NopMetrics struct{}
-func (NopMetrics) SubscriberConnected(s *Subscriber) {}
-func (NopMetrics) SubscriberDisconnected(s *Subscriber) {}
-func (NopMetrics) UpdatePublished(s *Update) {}
+func (NopMetrics) SubscriberConnected(_ *Subscriber) {}
+func (NopMetrics) SubscriberDisconnected(_ *Subscriber) {}
+func (NopMetrics) UpdatePublished(_ *Update) {}
// PrometheusMetrics store Hub collected metrics.
type PrometheusMetrics struct {
@@ -46,7 +50,7 @@ func NewPrometheusMetrics(registry prometheus.Registerer) *PrometheusMetrics {
),
subscribers: prometheus.NewGauge(
prometheus.GaugeOpts{
- Name: "mercure_subscribers",
+ Name: "mercure_subscribers_connected",
Help: "The current number of running subscribers",
},
),
@@ -77,18 +81,18 @@ func (m *PrometheusMetrics) Register(r *mux.Router) {
// Go-unrelated process metrics (memory usage, file descriptors, etc.).
m.registry.MustRegister(prometheus.NewProcessCollector(prometheus.ProcessCollectorOpts{}))
- r.Handle("/metrics", promhttp.HandlerFor(m.registry.(*prometheus.Registry), promhttp.HandlerOpts{})).Methods("GET")
+ r.Handle(metricsPath, promhttp.HandlerFor(m.registry.(*prometheus.Registry), promhttp.HandlerOpts{})).Methods(http.MethodGet)
}
-func (m *PrometheusMetrics) SubscriberConnected(s *Subscriber) {
+func (m *PrometheusMetrics) SubscriberConnected(_ *Subscriber) {
m.subscribersTotal.Inc()
m.subscribers.Inc()
}
-func (m *PrometheusMetrics) SubscriberDisconnected(s *Subscriber) {
+func (m *PrometheusMetrics) SubscriberDisconnected(_ *Subscriber) {
m.subscribers.Dec()
}
-func (m *PrometheusMetrics) UpdatePublished(u *Update) {
+func (m *PrometheusMetrics) UpdatePublished(_ *Update) {
m.updatesTotal.Inc()
}
diff --git a/metrics_test.go b/metrics_test.go
index f4564aae..ca183b34 100644
--- a/metrics_test.go
+++ b/metrics_test.go
@@ -75,7 +75,7 @@ func assertGaugeValue(t *testing.T, v float64, g prometheus.Gauge) {
t.Fatal(err)
}
- assert.Equal(t, v, *metricOut.Gauge.Value)
+ assert.Equal(t, v, metricOut.GetGauge().GetValue()) //nolint:testifylint
}
func assertCounterValue(t *testing.T, v float64, c prometheus.Counter) {
@@ -86,5 +86,5 @@ func assertCounterValue(t *testing.T, v float64, c prometheus.Counter) {
t.Fatal(err)
}
- assert.Equal(t, v, *metricOut.Counter.Value)
+ assert.Equal(t, v, metricOut.GetCounter().GetValue()) // nolint:testifylint
}
diff --git a/public/app.js b/public/app.js
index ef680b9c..7ecd8709 100644
--- a/public/app.js
+++ b/public/app.js
@@ -1,259 +1,278 @@
-"use strict";
+'use strict';
+
+/* eslint-env browser */
+/* global EventSourcePolyfill */
(function () {
- const origin = window.location.origin;
-
- const defaultTopic = document.URL + "demo/books/1.jsonld";
- const placeholderTopic = "https://example.com/my-private-topic";
+ const origin = window.location.origin
+ const defaultTopic = document.URL + 'demo/books/1.jsonld'
+ const placeholderTopic = 'https://example.com/my-private-topic'
const defaultJwt =
- "eyJhbGciOiJIUzI1NiJ9.eyJtZXJjdXJlIjp7InB1Ymxpc2giOlsiKiJdLCJzdWJzY3JpYmUiOlsiaHR0cHM6Ly9leGFtcGxlLmNvbS9teS1wcml2YXRlLXRvcGljIiwie3NjaGVtZX06Ly97K2hvc3R9L2RlbW8vYm9va3Mve2lkfS5qc29ubGQiLCIvLndlbGwta25vd24vbWVyY3VyZS9zdWJzY3JpcHRpb25zey90b3BpY317L3N1YnNjcmliZXJ9Il0sInBheWxvYWQiOnsidXNlciI6Imh0dHBzOi8vZXhhbXBsZS5jb20vdXNlcnMvZHVuZ2xhcyIsInJlbW90ZUFkZHIiOiIxMjcuMC4wLjEifX19.z5YrkHwtkz3O_nOnhC_FP7_bmeISe3eykAkGbAl5K7c";
+ 'eyJhbGciOiJIUzI1NiJ9.eyJtZXJjdXJlIjp7InB1Ymxpc2giOlsiKiJdLCJzdWJzY3JpYmUiOlsiaHR0cHM6Ly9leGFtcGxlLmNvbS9teS1wcml2YXRlLXRvcGljIiwie3NjaGVtZX06Ly97K2hvc3R9L2RlbW8vYm9va3Mve2lkfS5qc29ubGQiLCIvLndlbGwta25vd24vbWVyY3VyZS9zdWJzY3JpcHRpb25zey90b3BpY317L3N1YnNjcmliZXJ9Il0sInBheWxvYWQiOnsidXNlciI6Imh0dHBzOi8vZXhhbXBsZS5jb20vdXNlcnMvZHVuZ2xhcyIsInJlbW90ZUFkZHIiOiIxMjcuMC4wLjEifX19.KKPIikwUzRuB3DTpVw6ajzwSChwFw5omBMmMcWKiDcM'
- const $updates = document.getElementById("updates");
- const $subscriptions = document.getElementById("subscriptions");
- const $settingsForm = document.forms.settings;
- const $discoverForm = document.forms.discover;
- const $subscribeForm = document.forms.subscribe;
- const $publishForm = document.forms.publish;
- const $subscriptionsForm = document.forms.subscriptions;
+ const $updates = document.getElementById('updates')
+ const $subscriptions = document.getElementById('subscriptions')
+ const $settingsForm = document.forms.settings
+ const $discoverForm = document.forms.discover
+ const $subscribeForm = document.forms.subscribe
+ const $publishForm = document.forms.publish
+ const $subscriptionsForm = document.forms.subscriptions
const error = (e) => {
- const message = e.toString();
- console.error(e);
- alert(message);
- };
+ if (!e.error || e.error.message?.includes?.('Reconnecting')) {
+ // Silent reconnecting messages from the polyfill
+
+ console.log('Connection closed, reconnecting...', e)
+
+ return
+ }
+
+ console.log(e)
+
+ if (e.toString !== Object.prototype.toString) {
+ // Display relevant error message
+ alert(e.toString())
+
+ return
+ }
+
+ if (e.statusText) {
+ // Special handling of errors from the polyfill
+ alert(e.statusText)
+
+ return
+ }
+
+ alert('An error occured, details have been logged.')
+ }
const getHubUrl = (resp) => {
- const link = resp.headers.get("Link");
+ const link = resp.headers.get('Link')
if (!link) {
- error('No rel="mercure" Link header provided.');
+ error('No rel="mercure" Link header provided.')
}
- const match = link.match(/<(.*)>.*rel="mercure".*/);
- if (match && match[1]) return match[1];
- };
+ const match = link.match(/<(.*)>.*rel="mercure".*/)
+ if (match && match[1]) return match[1]
+ }
// Set default values
- document.addEventListener("DOMContentLoaded", () => {
- $settingsForm.hubUrl.value = origin + "/.well-known/mercure";
- $settingsForm.jwt.value = defaultJwt;
+ document.addEventListener('DOMContentLoaded', () => {
+ $settingsForm.hubUrl.value = origin + '/.well-known/mercure'
+ $settingsForm.jwt.value = defaultJwt
- $discoverForm.topic.value = defaultTopic;
+ $discoverForm.topic.value = defaultTopic
$discoverForm.body.value = JSON.stringify(
{
- "@id": defaultTopic,
- availability: "https://schema.org/InStock",
+ '@id': defaultTopic,
+ availability: 'https://schema.org/InStock'
},
null,
2
- );
+ )
$publishForm.data.value = JSON.stringify(
{
- "@id": defaultTopic,
- availability: "https://schema.org/OutOfStock",
+ '@id': defaultTopic,
+ availability: 'https://schema.org/OutOfStock'
},
null,
2
- );
+ )
document.getElementById(
- "subscribeTopicsExamples"
+ 'subscribeTopicsExamples'
).textContent = `${document.URL}demo/novels/{id}.jsonld
${defaultTopic}
-foo`;
- });
+foo`
+ })
// Discover
$discoverForm.onsubmit = async function (e) {
- e.preventDefault();
+ e.preventDefault()
const {
- elements: { topic, body },
- } = this;
- const jwt = $settingsForm.jwt.value;
+ elements: { topic, body }
+ } = this
+ const jwt = $settingsForm.jwt.value
- const url = new URL(topic.value);
- if (body.value) url.searchParams.append("body", body.value);
- if (jwt) url.searchParams.append("jwt", jwt);
+ const url = new URL(topic.value)
+ if (body.value) url.searchParams.append('body', body.value)
+ if (jwt) url.searchParams.append('jwt', jwt)
try {
- const resp = await fetch(url);
- if (!resp.ok) throw new Error(resp.statusText);
+ const resp = await fetch(url)
+ if (!resp.ok) throw new Error(resp.statusText)
// Set hub default
- const hubUrl = getHubUrl(resp);
- if (hubUrl) $settingsForm.hubUrl.value = new URL(hubUrl, topic.value);
+ const hubUrl = getHubUrl(resp)
+ if (hubUrl) $settingsForm.hubUrl.value = new URL(hubUrl, topic.value)
- const subscribeTopics = $subscribeForm.topics;
- if (subscribeTopics.value === placeholderTopic)
- subscribeTopics.value = topic.value;
+ const subscribeTopics = $subscribeForm.topics
+ if (subscribeTopics.value === placeholderTopic) { subscribeTopics.value = topic.value }
// Set publish default values
- const publishTopics = $publishForm.topics;
- if (publishTopics.value === placeholderTopic)
- publishTopics.value = topic.value;
+ const publishTopics = $publishForm.topics
+ if (publishTopics.value === placeholderTopic) { publishTopics.value = topic.value }
- const text = await resp.text();
- body.value = text;
+ const text = await resp.text()
+ body.value = text
} catch (e) {
- error(e);
+ error(e)
}
- };
+ }
// Subscribe
- const $updateTemplate = document.getElementById("update");
- let updateEventSource;
+ const $updateTemplate = document.getElementById('update')
+ let updateEventSource
$subscribeForm.onsubmit = function (e) {
- e.preventDefault();
+ e.preventDefault()
- updateEventSource && updateEventSource.close();
- $updates.textContent = "No updates pushed yet.";
+ updateEventSource && updateEventSource.close()
+ $updates.textContent = 'No updates pushed yet.'
const {
- elements: { topics, lastEventId },
- } = this;
+ elements: { topics, lastEventId }
+ } = this
- const topicList = topics.value.split("\n");
- const u = new URL($settingsForm.hubUrl.value);
- topicList.forEach((topic) => u.searchParams.append("topic", topic));
- if (lastEventId.value)
- u.searchParams.append("Last-Event-ID", lastEventId.value);
+ const topicList = topics.value.split('\n')
+ const u = new URL($settingsForm.hubUrl.value)
+ topicList.forEach((topic) => u.searchParams.append('topic', topic))
+ if (lastEventId.value) { u.searchParams.append('lastEventID', lastEventId.value) }
- let ol = null;
- if ($settingsForm.authorization.value === "header")
+ let ol = null
+ if ($settingsForm.authorization.value === 'header') {
updateEventSource = new EventSourcePolyfill(u, {
headers: {
- Authorization: `Bearer ${$settingsForm.jwt.value}`,
- },
- });
- else updateEventSource = new EventSource(u);
+ Authorization: `Bearer ${$settingsForm.jwt.value}`
+ }
+ })
+ } else updateEventSource = new EventSource(u)
updateEventSource.onmessage = function (e) {
if (!ol) {
- ol = document.createElement("ol");
- ol.reversed = true;
+ ol = document.createElement('ol')
+ ol.reversed = true
- $updates.textContent = "";
- $updates.appendChild(ol);
+ $updates.textContent = ''
+ $updates.appendChild(ol)
}
- const li = document.importNode($updateTemplate.content, true);
- li.querySelector("h2").textContent = e.lastEventId;
- li.querySelector("pre").textContent = e.data;
- ol.firstChild ? ol.insertBefore(li, ol.firstChild) : ol.appendChild(li);
- };
- updateEventSource.onerror = console.log;
- this.elements.unsubscribe.disabled = false;
- };
+ const li = document.importNode($updateTemplate.content, true)
+ li.querySelector('h2').textContent = e.lastEventId
+ li.querySelector('pre').textContent = e.data
+ ol.firstChild ? ol.insertBefore(li, ol.firstChild) : ol.appendChild(li)
+ }
+ updateEventSource.onerror = error
+ this.elements.unsubscribe.disabled = false
+ }
$subscribeForm.elements.unsubscribe.onclick = function (e) {
- e.preventDefault();
+ e.preventDefault()
- updateEventSource.close();
- this.disabled = true;
- $updates.textContent = "Unsubscribed.";
- };
+ updateEventSource && updateEventSource.close()
+ this.disabled = true
+ $updates.textContent = 'Unsubscribed.'
+ }
// Publish
$publishForm.onsubmit = async function (e) {
- e.preventDefault();
+ e.preventDefault()
const {
- elements: { topics, data, priv, id, type, retry },
- } = this;
+ elements: { topics, data, priv, id, type, retry }
+ } = this
const body = new URLSearchParams({
data: data.value,
id: id.value,
type: type.value,
- retry: retry.value,
- });
+ retry: retry.value
+ })
- topics.value.split("\n").forEach((topic) => body.append("topic", topic));
- priv.checked && body.append("private", "on");
+ topics.value.split('\n').forEach((topic) => body.append('topic', topic))
+ priv.checked && body.append('private', 'on')
- const opt = { method: "POST", body };
- if ($settingsForm.authorization.value === "header")
- opt.headers = { Authorization: `Bearer ${$settingsForm.jwt.value}` };
+ const opt = { method: 'POST', body }
+ if ($settingsForm.authorization.value === 'header') { opt.headers = { Authorization: `Bearer ${$settingsForm.jwt.value}` } }
try {
- const resp = await fetch($settingsForm.hubUrl.value, opt);
- if (!resp.ok) throw new Error(resp.statusText);
+ const resp = await fetch($settingsForm.hubUrl.value, opt)
+ if (!resp.ok) throw new Error(resp.statusText)
} catch (e) {
- error(e);
+ error(e)
}
- };
+ }
// Subscriptions
- const $subscriptionTemplate = document.getElementById("subscription");
- let subscriptionEventSource;
+ const $subscriptionTemplate = document.getElementById('subscription')
+ let subscriptionEventSource
const addSubscription = (s) => {
const subscription = document.importNode(
$subscriptionTemplate.content,
true
- );
- subscription.querySelector("div").setAttribute("id", s.id);
- subscription.querySelector(".card-header-title").textContent = s.id;
- subscription.querySelector(".topic").textContent = s.topic;
- subscription.querySelector(".subscriber").textContent = s.subscriber;
- subscription.querySelector("code").textContent = JSON.stringify(
+ )
+ subscription.querySelector('div').setAttribute('id', s.id)
+ subscription.querySelector('.card-header-title').textContent = s.id
+ subscription.querySelector('.topic').textContent = s.topic
+ subscription.querySelector('.subscriber').textContent = s.subscriber
+ subscription.querySelector('code').textContent = JSON.stringify(
s.payload,
null,
2
- );
- $subscriptions.appendChild(subscription);
- };
+ )
+ $subscriptions.appendChild(subscription)
+ }
$subscriptionsForm.onsubmit = async (e) => {
- e.preventDefault();
+ e.preventDefault()
- subscriptionEventSource && subscriptionEventSource.close();
- $subscriptions.textContent = "";
+ subscriptionEventSource && subscriptionEventSource.close()
+ $subscriptions.textContent = ''
try {
const opt =
- $settingsForm.authorization.value === "header"
+ $settingsForm.authorization.value === 'header'
? { headers: { Authorization: `Bearer ${$settingsForm.jwt.value}` } }
- : undefined;
+ : undefined
const resp = await fetch(
`${$settingsForm.hubUrl.value}/subscriptions`,
opt
- );
- if (!resp.ok) throw new Error(resp.statusText);
- const json = await resp.json();
+ )
+ if (!resp.ok) throw new Error(resp.statusText)
+ const json = await resp.json()
- json.subscriptions.forEach(addSubscription);
+ json.subscriptions.forEach(addSubscription)
- const u = new URL($settingsForm.hubUrl.value);
+ const u = new URL($settingsForm.hubUrl.value)
u.searchParams.append(
- "topic",
- "/.well-known/mercure/subscriptions{/topic}{/subscriber}"
- );
- u.searchParams.append("Last-Event-ID", json.lastEventID);
+ 'topic',
+ '/.well-known/mercure/subscriptions{/topic}{/subscriber}'
+ )
+ u.searchParams.append('lastEventID', json.lastEventID)
- if (opt) subscriptionEventSource = new EventSourcePolyfill(u, opt);
- else subscriptionEventSource = new EventSource(u);
+ if (opt) subscriptionEventSource = new EventSourcePolyfill(u, opt)
+ else subscriptionEventSource = new EventSource(u)
subscriptionEventSource.onmessage = function (e) {
- const s = JSON.parse(e.data);
+ const s = JSON.parse(e.data)
if (s.active) {
- addSubscription(s);
- return;
+ addSubscription(s)
+ return
}
- document.getElementById(s.id).remove();
- };
- subscriptionEventSource.onerror = console.log;
+ document.getElementById(s.id).remove()
+ }
+ subscriptionEventSource.onerror = error
- $subscriptionsForm.elements.unsubscribe.disabled = false;
+ $subscriptionsForm.elements.unsubscribe.disabled = false
} catch (e) {
- error(e);
- return;
+ error(e)
}
- };
+ }
$subscriptionsForm.elements.unsubscribe.onclick = function (e) {
- e.preventDefault();
+ e.preventDefault()
- subscriptionEventSource.close();
- this.disabled = true;
- $subscriptions.textContent = "";
- };
-})();
+ subscriptionEventSource.close()
+ this.disabled = true
+ $subscriptions.textContent = ''
+ }
+})()
diff --git a/public/index.html b/public/index.html
index a2f44866..4c5f02e2 100644
--- a/public/index.html
+++ b/public/index.html
@@ -4,7 +4,7 @@
Mercure
-
+
@@ -188,10 +188,10 @@ Settings
}
}
- Create a
token
- (demo key: !ChangeMe!
)
+ (demo key: !ChangeThisMercureHubJWTSecretKey!
)
diff --git a/publish.go b/publish.go
index f1c38c38..b5efcc6d 100644
--- a/publish.go
+++ b/publish.go
@@ -9,16 +9,15 @@ import (
)
// PublishHandler allows publisher to broadcast updates to all subscribers.
+//
+//nolint:funlen
func (h *Hub) PublishHandler(w http.ResponseWriter, r *http.Request) {
var claims *claims
var err error
- if h.publisherJWT != nil {
- claims, err = authorize(r, h.publisherJWT, h.publishOrigins, h.cookieName)
+ if h.publisherJWTKeyFunc != nil {
+ claims, err = authorize(r, h.publisherJWTKeyFunc, h.publishOrigins, h.cookieName)
if err != nil || claims == nil || claims.Mercure.Publish == nil {
- http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
- if c := h.logger.Check(zap.InfoLevel, "Topic selectors not matched, not provided or authorization error"); c != nil {
- c.Write(zap.String("remote_addr", r.RemoteAddr), zap.Error(err))
- }
+ h.httpAuthorizationError(w, r, err)
return
}
@@ -47,10 +46,21 @@ func (h *Hub) PublishHandler(w http.ResponseWriter, r *http.Request) {
}
private := len(r.PostForm["private"]) != 0
- if private && !canDispatch(h.topicSelectorStore, topics, claims.Mercure.Publish) {
- http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
+ if !canDispatch(h.topicSelectorStore, topics, claims.Mercure.Publish) {
+ if private {
+ http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
- return
+ return
+ }
+
+ if h.isBackwardCompatiblyEnabledWith(7) {
+ h.logger.Info("Deprecated: posting public updates to topics not listed in the 'mercure.publish' JWT claim is deprecated since the version 7 of the protocol, use '[\"*\"]' as value to allow publishing on all topics.")
+ } else {
+ h.logger.Info("Unsupported: posting public updates to topics not listed in the 'mercure.publish' JWT claim is not supported anymore, use '[\"*\"]' as value to allow publishing on all topics or enable backward compatibility with the version 7 of the protocol.")
+ http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
+
+ return
+ }
}
u := &Update{
@@ -66,7 +76,7 @@ func (h *Hub) PublishHandler(w http.ResponseWriter, r *http.Request) {
}
io.WriteString(w, u.ID)
- if c := h.logger.Check(zap.InfoLevel, "Update published"); c != nil {
+ if c := h.logger.Check(zap.DebugLevel, "Update published"); c != nil {
c.Write(zap.Object("update", u), zap.String("remote_addr", r.RemoteAddr))
}
h.metrics.UpdatePublished(u)
diff --git a/publish_test.go b/publish_test.go
index 1fef9dfd..38da318f 100644
--- a/publish_test.go
+++ b/publish_test.go
@@ -1,7 +1,7 @@
package mercure
import (
- "io/ioutil"
+ "io"
"net/http"
"net/http/httptest"
"net/url"
@@ -15,10 +15,10 @@ import (
"go.uber.org/zap"
)
-func TestNoAuthorizationHeader(t *testing.T) {
+func TestPublishNoAuthorizationHeader(t *testing.T) {
hub := createDummy()
- req := httptest.NewRequest("POST", defaultHubURL, nil)
+ req := httptest.NewRequest(http.MethodPost, defaultHubURL, nil)
w := httptest.NewRecorder()
hub.PublishHandler(w, req)
@@ -32,8 +32,8 @@ func TestNoAuthorizationHeader(t *testing.T) {
func TestPublishUnauthorizedJWT(t *testing.T) {
hub := createDummy()
- req := httptest.NewRequest("POST", defaultHubURL, nil)
- req.Header.Add("Authorization", "Bearer "+createDummyUnauthorizedJWT())
+ req := httptest.NewRequest(http.MethodPost, defaultHubURL, nil)
+ req.Header.Add("Authorization", bearerPrefix+createDummyUnauthorizedJWT())
w := httptest.NewRecorder()
hub.PublishHandler(w, req)
@@ -47,8 +47,8 @@ func TestPublishUnauthorizedJWT(t *testing.T) {
func TestPublishInvalidAlgJWT(t *testing.T) {
hub := createDummy()
- req := httptest.NewRequest("POST", defaultHubURL, nil)
- req.Header.Add("Authorization", "Bearer "+createDummyNoneSignedJWT())
+ req := httptest.NewRequest(http.MethodPost, defaultHubURL, nil)
+ req.Header.Add("Authorization", bearerPrefix+createDummyNoneSignedJWT())
w := httptest.NewRecorder()
hub.PublishHandler(w, req)
@@ -62,8 +62,8 @@ func TestPublishInvalidAlgJWT(t *testing.T) {
func TestPublishBadContentType(t *testing.T) {
hub := createDummy()
- req := httptest.NewRequest("POST", defaultHubURL, nil)
- req.Header.Add("Authorization", "Bearer "+createDummyAuthorizedJWT(hub, rolePublisher, []string{}))
+ req := httptest.NewRequest(http.MethodPost, defaultHubURL, nil)
+ req.Header.Add("Authorization", bearerPrefix+createDummyAuthorizedJWT(rolePublisher, []string{"*"}))
req.Header.Add("Content-Type", "text/plain; boundary=")
w := httptest.NewRecorder()
hub.PublishHandler(w, req)
@@ -77,8 +77,8 @@ func TestPublishBadContentType(t *testing.T) {
func TestPublishNoTopic(t *testing.T) {
hub := createDummy()
- req := httptest.NewRequest("POST", defaultHubURL, nil)
- req.Header.Add("Authorization", "Bearer "+createDummyAuthorizedJWT(hub, rolePublisher, []string{}))
+ req := httptest.NewRequest(http.MethodPost, defaultHubURL, nil)
+ req.Header.Add("Authorization", bearerPrefix+createDummyAuthorizedJWT(rolePublisher, []string{"*"}))
w := httptest.NewRecorder()
hub.PublishHandler(w, req)
@@ -89,15 +89,17 @@ func TestPublishNoTopic(t *testing.T) {
assert.Equal(t, "Missing \"topic\" parameter\n", w.Body.String())
}
-func TestPublishNoData(t *testing.T) {
+func TestPublishInvalidRetry(t *testing.T) {
hub := createDummy()
form := url.Values{}
form.Add("topic", "http://example.com/books/1")
+ form.Add("data", "foo")
+ form.Add("retry", "invalid")
- req := httptest.NewRequest("POST", defaultHubURL, strings.NewReader(form.Encode()))
+ req := httptest.NewRequest(http.MethodPost, defaultHubURL, strings.NewReader(form.Encode()))
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
- req.Header.Add("Authorization", "Bearer "+createDummyAuthorizedJWT(hub, rolePublisher, []string{"*"}))
+ req.Header.Add("Authorization", bearerPrefix+createDummyAuthorizedJWT(rolePublisher, []string{"*"}))
w := httptest.NewRecorder()
hub.PublishHandler(w, req)
@@ -105,20 +107,21 @@ func TestPublishNoData(t *testing.T) {
resp := w.Result()
defer resp.Body.Close()
- assert.Equal(t, http.StatusOK, resp.StatusCode)
+ assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
+ assert.Equal(t, "Invalid \"retry\" parameter\n", w.Body.String())
}
-func TestPublishInvalidRetry(t *testing.T) {
+func TestPublishNotAuthorizedTopicSelector(t *testing.T) {
hub := createDummy()
form := url.Values{}
form.Add("topic", "http://example.com/books/1")
form.Add("data", "foo")
- form.Add("retry", "invalid")
+ form.Add("private", "on")
- req := httptest.NewRequest("POST", defaultHubURL, strings.NewReader(form.Encode()))
+ req := httptest.NewRequest(http.MethodPost, defaultHubURL, strings.NewReader(form.Encode()))
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
- req.Header.Add("Authorization", "Bearer "+createDummyAuthorizedJWT(hub, rolePublisher, []string{}))
+ req.Header.Add("Authorization", bearerPrefix+createDummyAuthorizedJWT(rolePublisher, []string{"foo"}))
w := httptest.NewRecorder()
hub.PublishHandler(w, req)
@@ -126,21 +129,18 @@ func TestPublishInvalidRetry(t *testing.T) {
resp := w.Result()
defer resp.Body.Close()
- assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
- assert.Equal(t, "Invalid \"retry\" parameter\n", w.Body.String())
+ assert.Equal(t, http.StatusUnauthorized, resp.StatusCode)
}
-func TestPublishNotAuthorizedTopicSelector(t *testing.T) {
+func TestPublishEmptyTopicSelector(t *testing.T) {
hub := createDummy()
form := url.Values{}
form.Add("topic", "http://example.com/books/1")
- form.Add("data", "foo")
- form.Add("private", "on")
- req := httptest.NewRequest("POST", defaultHubURL, strings.NewReader(form.Encode()))
+ req := httptest.NewRequest(http.MethodPost, defaultHubURL, strings.NewReader(form.Encode()))
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
- req.Header.Add("Authorization", "Bearer "+createDummyAuthorizedJWT(hub, rolePublisher, []string{"foo"}))
+ req.Header.Add("Authorization", bearerPrefix+createDummyAuthorizedJWT(rolePublisher, []string{}))
w := httptest.NewRecorder()
hub.PublishHandler(w, req)
@@ -151,6 +151,25 @@ func TestPublishNotAuthorizedTopicSelector(t *testing.T) {
assert.Equal(t, http.StatusUnauthorized, resp.StatusCode)
}
+func TestPublishLegacyAuthorization(t *testing.T) {
+ hub := createDummy(WithProtocolVersionCompatibility(7))
+
+ form := url.Values{}
+ form.Add("topic", "http://example.com/books/1")
+
+ req := httptest.NewRequest(http.MethodPost, defaultHubURL, strings.NewReader(form.Encode()))
+ req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
+ req.Header.Add("Authorization", bearerPrefix+createDummyAuthorizedJWT(rolePublisher, []string{}))
+
+ w := httptest.NewRecorder()
+ hub.PublishHandler(w, req)
+
+ resp := w.Result()
+ defer resp.Body.Close()
+
+ assert.Equal(t, http.StatusOK, resp.StatusCode)
+}
+
func TestPublishOK(t *testing.T) {
hub := createDummy()
@@ -159,7 +178,7 @@ func TestPublishOK(t *testing.T) {
s.SetTopics(topics, topics)
s.Claims = &claims{Mercure: mercureClaim{Subscribe: topics}}
- require.Nil(t, hub.transport.AddSubscriber(s))
+ require.NoError(t, hub.transport.AddSubscriber(s))
var wg sync.WaitGroup
wg.Add(1)
@@ -167,9 +186,9 @@ func TestPublishOK(t *testing.T) {
defer w.Done()
u, ok := <-s.Receive()
assert.True(t, ok)
- require.NotNil(t, u)
+ assert.NotNil(t, u)
assert.Equal(t, "id", u.ID)
- assert.Equal(t, s.Topics, u.Topics)
+ assert.Equal(t, s.SubscribedTopics, u.Topics)
assert.Equal(t, "Hello!", u.Data)
assert.True(t, u.Private)
}(&wg)
@@ -180,16 +199,16 @@ func TestPublishOK(t *testing.T) {
form.Add("data", "Hello!")
form.Add("private", "on")
- req := httptest.NewRequest("POST", defaultHubURL, strings.NewReader(form.Encode()))
+ req := httptest.NewRequest(http.MethodPost, defaultHubURL, strings.NewReader(form.Encode()))
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
- req.Header.Add("Authorization", "Bearer "+createDummyAuthorizedJWT(hub, rolePublisher, s.Topics))
+ req.Header.Add("Authorization", bearerPrefix+createDummyAuthorizedJWT(rolePublisher, s.SubscribedTopics))
w := httptest.NewRecorder()
hub.PublishHandler(w, req)
resp := w.Result()
defer resp.Body.Close()
- body, _ := ioutil.ReadAll(resp.Body)
+ body, _ := io.ReadAll(resp.Body)
assert.Equal(t, http.StatusOK, resp.StatusCode)
assert.Equal(t, "id", string(body))
@@ -197,32 +216,51 @@ func TestPublishOK(t *testing.T) {
wg.Wait()
}
+func TestPublishNoData(t *testing.T) {
+ hub := createDummy()
+
+ form := url.Values{}
+ form.Add("topic", "http://example.com/books/1")
+
+ req := httptest.NewRequest(http.MethodPost, defaultHubURL, strings.NewReader(form.Encode()))
+ req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
+ req.Header.Add("Authorization", bearerPrefix+createDummyAuthorizedJWT(rolePublisher, []string{"*"}))
+
+ w := httptest.NewRecorder()
+ hub.PublishHandler(w, req)
+
+ resp := w.Result()
+ defer resp.Body.Close()
+
+ assert.Equal(t, http.StatusOK, resp.StatusCode)
+}
+
func TestPublishGenerateUUID(t *testing.T) {
h := createDummy()
s := NewSubscriber("", zap.NewNop())
- s.SetTopics([]string{"http://example.com/books/1"}, s.Topics)
+ s.SetTopics([]string{"http://example.com/books/1"}, s.SubscribedTopics)
- require.Nil(t, h.transport.AddSubscriber(s))
+ require.NoError(t, h.transport.AddSubscriber(s))
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
u := <-s.Receive()
- require.NotNil(t, u)
+ assert.NotNil(t, u)
_, err := uuid.FromString(strings.TrimPrefix(u.ID, "urn:uuid:"))
- assert.Nil(t, err)
+ assert.NoError(t, err)
}()
form := url.Values{}
form.Add("topic", "http://example.com/books/1")
form.Add("data", "Hello!")
- req := httptest.NewRequest("POST", defaultHubURL, strings.NewReader(form.Encode()))
+ req := httptest.NewRequest(http.MethodPost, defaultHubURL, strings.NewReader(form.Encode()))
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
- req.Header.Add("Authorization", "Bearer "+createDummyAuthorizedJWT(h, rolePublisher, []string{}))
+ req.Header.Add("Authorization", bearerPrefix+createDummyAuthorizedJWT(rolePublisher, []string{"*"}))
w := httptest.NewRecorder()
h.PublishHandler(w, req)
@@ -231,11 +269,11 @@ func TestPublishGenerateUUID(t *testing.T) {
defer resp.Body.Close()
assert.Equal(t, http.StatusOK, resp.StatusCode)
- bodyBytes, _ := ioutil.ReadAll(resp.Body)
+ bodyBytes, _ := io.ReadAll(resp.Body)
body := string(bodyBytes)
_, err := uuid.FromString(strings.TrimPrefix(body, "urn:uuid:"))
- assert.Nil(t, err)
+ require.NoError(t, err)
wg.Wait()
}
@@ -256,17 +294,66 @@ func TestPublishWithErrorInTransport(t *testing.T) {
form.Add("data", "Hello!")
form.Add("private", "on")
- req := httptest.NewRequest("POST", defaultHubURL, strings.NewReader(form.Encode()))
+ req := httptest.NewRequest(http.MethodPost, defaultHubURL, strings.NewReader(form.Encode()))
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
- req.Header.Add("Authorization", "Bearer "+createDummyAuthorizedJWT(hub, rolePublisher, []string{"foo", "http://example.com/books/1"}))
+ req.Header.Add("Authorization", bearerPrefix+createDummyAuthorizedJWT(rolePublisher, []string{"foo", "http://example.com/books/1"}))
w := httptest.NewRecorder()
hub.PublishHandler(w, req)
resp := w.Result()
defer resp.Body.Close()
- body, _ := ioutil.ReadAll(resp.Body)
+ body, _ := io.ReadAll(resp.Body)
assert.Equal(t, http.StatusOK, resp.StatusCode)
assert.Equal(t, "id", string(body))
}
+
+func FuzzPublish(f *testing.F) {
+ hub := createDummy()
+ authorizationHeader := bearerPrefix + createDummyAuthorizedJWT(rolePublisher, []string{"*"})
+
+ testCases := [][]interface{}{
+ {"https://localhost/foo/bar", "baz", "", "", "", "", ""},
+ {"https://localhost/foo/baz", "bat", "id", "data", "on", "22", "mytype"},
+ }
+
+ for _, tc := range testCases {
+ f.Add(tc...) //nolint:govet
+ }
+
+ f.Fuzz(func(t *testing.T, topic1, topic2, id, data, private, retry, typ string) {
+ form := url.Values{}
+ form.Add("topic", topic1)
+ form.Add("topic", topic2)
+ form.Add("id", id)
+ form.Add("data", data)
+ form.Add("private", private)
+ form.Add("retry", retry)
+ form.Add("type", typ)
+
+ req := httptest.NewRequest(http.MethodPost, defaultHubURL, strings.NewReader(form.Encode()))
+ req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
+ req.Header.Add("Authorization", authorizationHeader)
+
+ w := httptest.NewRecorder()
+ hub.PublishHandler(w, req)
+
+ resp := w.Result()
+ defer resp.Body.Close()
+ body, _ := io.ReadAll(resp.Body)
+
+ if resp.StatusCode == http.StatusBadRequest {
+ return
+ }
+
+ assert.Equal(t, http.StatusOK, resp.StatusCode)
+ if id == "" {
+ assert.NotEqual(t, "", string(body))
+
+ return
+ }
+
+ assert.Equal(t, id, string(body))
+ })
+}
diff --git a/redis_transport.go b/redis_transport.go
index 6890f3d4..139bdae6 100644
--- a/redis_transport.go
+++ b/redis_transport.go
@@ -9,7 +9,7 @@ import (
"strconv"
"sync"
- redis "github.com/go-redis/redis/v8"
+ redis "github.com/redis/go-redis/v9"
"go.uber.org/zap"
)
@@ -37,7 +37,7 @@ type RedisTransport struct {
}
// NewRedisTransport create a new RedisTransport.
-func NewRedisTransport(u *url.URL, l Logger, tss *TopicSelectorStore) (Transport, error) { //nolint:ireturn
+func NewRedisTransport(u *url.URL, l Logger) (Transport, error) { //nolint:ireturn
var err error
q := u.Query()
bucketName := defaultRedisBucketName
@@ -250,20 +250,10 @@ func (t *RedisTransport) RemoveSubscriber(s *Subscriber) error {
// GetSubscribers get the list of active subscribers.
func (t *RedisTransport) GetSubscribers() (string, []*Subscriber, error) {
- t.logger.Info("GetSubscribers:RLock")
t.RLock()
defer t.RUnlock()
- t.logger.Info("GetSubscribers:append")
- var subscribers []*Subscriber
- t.subscribers.Walk(0, func(s *Subscriber) bool {
- subscribers = append(subscribers, s)
-
- return true
- })
- t.logger.Info("GetSubscribers:return")
-
- return t.lastEventID, subscribers, nil
+ return t.lastEventID, getSubscribers(t.subscribers), nil
}
func (t *RedisTransport) dispatchHistory(s *Subscriber) {
diff --git a/server_test.go b/server_test.go
index 2879efbe..1745e4cc 100644
--- a/server_test.go
+++ b/server_test.go
@@ -5,7 +5,6 @@ import (
"crypto/tls"
"errors"
"io"
- "io/ioutil"
"net/http"
"net/url"
"os"
@@ -23,8 +22,11 @@ import (
)
const (
- testURL = "http://" + testAddr + defaultHubURL
- testSecureURL = "https://" + testAddr + defaultHubURL
+ testURLscheme = "http://"
+ testURL = testURLscheme + testAddr + defaultHubURL
+
+ testSecureURLScheme = "https://"
+ testSecureURL = testSecureURLScheme + testAddr + defaultHubURL
)
func TestForwardedHeaders(t *testing.T) {
@@ -44,16 +46,16 @@ func TestForwardedHeaders(t *testing.T) {
defer resp.Body.Close()
body := url.Values{"topic": {"http://example.com/test-forwarded"}, "data": {"hello"}}
- req, _ := http.NewRequest("POST", testURL, strings.NewReader(body.Encode()))
+ req, _ := http.NewRequest(http.MethodPost, testURL, strings.NewReader(body.Encode()))
req.Header.Add("X-Forwarded-For", "192.0.2.1")
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
- req.Header.Add("Authorization", "Bearer "+createDummyAuthorizedJWT(h, rolePublisher, []string{}))
+ req.Header.Add("Authorization", bearerPrefix+createDummyAuthorizedJWT(rolePublisher, []string{"*"}))
resp2, err := client.Do(req)
- require.Nil(t, err)
+ require.NoError(t, err)
defer resp2.Body.Close()
- assert.True(t, logs.FilterField(zap.String("remote_addr", "192.0.2.1")).Len() == 1)
+ assert.Equal(t, 1, logs.FilterField(zap.String("remote_addr", "192.0.2.1")).Len())
h.server.Shutdown(context.Background())
}
@@ -85,10 +87,10 @@ func TestSecurityOptions(t *testing.T) {
resp.Body.Close()
// Preflight request
- req, _ := http.NewRequest("OPTIONS", testSecureURL, nil)
+ req, _ := http.NewRequest(http.MethodOptions, testSecureURL, nil)
req.Header.Add("Origin", "https://example.com")
req.Header.Add("Access-Control-Request-Headers", "authorization,cache-control,last-event-id")
- req.Header.Add("Access-Control-Request-Method", "GET")
+ req.Header.Add("Access-Control-Request-Method", http.MethodGet)
resp2, _ := client.Do(req)
require.NotNil(t, resp2)
@@ -98,7 +100,7 @@ func TestSecurityOptions(t *testing.T) {
resp2.Body.Close()
// Subscriptions
- req, _ = http.NewRequest("GET", testSecureURL+"/subscriptions", nil)
+ req, _ = http.NewRequest(http.MethodGet, testSecureURL+subscriptionsPath, nil)
resp3, _ := client.Do(req)
require.NotNil(t, resp3)
assert.Equal(t, http.StatusUnauthorized, resp3.StatusCode)
@@ -133,16 +135,16 @@ func TestSecurityOptionsWithCorsOrigin(t *testing.T) {
assert.Equal(t, "1; mode=block", resp.Header.Get("X-Xss-Protection"))
resp.Body.Close()
- req, _ := http.NewRequest("OPTIONS", testSecureURL, nil)
+ req, _ := http.NewRequest(http.MethodOptions, testSecureURL, nil)
- req.Header.Add("Authorization", "Bearer "+createDummyAuthorizedJWT(h, roleSubscriber, []string{}))
+ req.Header.Add("Authorization", bearerPrefix+createDummyAuthorizedJWT(roleSubscriber, []string{}))
req.Header.Add("Content-Type", "text/plain; boundary=")
req.Header.Add("Origin", "https://subscriber.com")
req.Header.Add("Host", "subscriber.com")
req.Header.Add("Cache-Control", "no-cache")
req.Header.Add("Pragma", "no-cache")
req.Header.Add("Access-Control-Request-Headers", "authorization,cache-control,last-event-id")
- req.Header.Add("Access-Control-Request-Method", "GET")
+ req.Header.Add("Access-Control-Request-Method", http.MethodGet)
resp2, _ := client.Do(req)
require.NotNil(t, resp2)
@@ -163,17 +165,17 @@ func TestServe(t *testing.T) {
var resp *http.Response
client := http.Client{Timeout: 100 * time.Millisecond}
for resp == nil {
- resp, _ = client.Get("http://" + testAddr + "/") //nolint:bodyclose
+ resp, _ = client.Get(testURLscheme + testAddr + "/") //nolint:bodyclose
}
defer resp.Body.Close()
- hpBody, _ := ioutil.ReadAll(resp.Body)
+ hpBody, _ := io.ReadAll(resp.Body)
assert.Contains(t, string(hpBody), "Mercure Hub")
- respHealthz, err := client.Get("http://" + testAddr + "/healthz")
- require.Nil(t, err)
+ respHealthz, err := client.Get(testURLscheme + testAddr + "/healthz")
+ require.NoError(t, err)
defer respHealthz.Body.Close()
- healthzBody, _ := ioutil.ReadAll(respHealthz.Body)
+ healthzBody, _ := io.ReadAll(respHealthz.Body)
assert.Contains(t, string(healthzBody), "ok")
var wgConnected, wgTested sync.WaitGroup
@@ -183,11 +185,11 @@ func TestServe(t *testing.T) {
go func() {
defer wgTested.Done()
resp, err := client.Get(testURL + "?topic=http%3A%2F%2Fexample.com%2Ffoo%2F1")
- require.Nil(t, err)
+ assert.NoError(t, err)
wgConnected.Done()
defer resp.Body.Close()
- body, _ := ioutil.ReadAll(resp.Body)
+ body, _ := io.ReadAll(resp.Body)
assert.Equal(t, []byte(":\nid: first\ndata: hello\n\n"), body)
}()
@@ -195,11 +197,11 @@ func TestServe(t *testing.T) {
go func() {
defer wgTested.Done()
resp, err := client.Get(testURL + "?topic=http%3A%2F%2Fexample.com%2Falt%2F1")
- require.Nil(t, err)
+ assert.NoError(t, err)
wgConnected.Done()
defer resp.Body.Close()
- body, _ := ioutil.ReadAll(resp.Body)
+ body, _ := io.ReadAll(resp.Body)
assert.Equal(t, []byte(":\nid: first\ndata: hello\n\n"), body)
}()
@@ -207,12 +209,12 @@ func TestServe(t *testing.T) {
wgConnected.Wait()
body := url.Values{"topic": {"http://example.com/foo/1", "http://example.com/alt/1"}, "data": {"hello"}, "id": {"first"}}
- req, _ := http.NewRequest("POST", testURL, strings.NewReader(body.Encode()))
+ req, _ := http.NewRequest(http.MethodPost, testURL, strings.NewReader(body.Encode()))
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
- req.Header.Add("Authorization", "Bearer "+createDummyAuthorizedJWT(h, rolePublisher, []string{}))
+ req.Header.Add("Authorization", bearerPrefix+createDummyAuthorizedJWT(rolePublisher, []string{"*"}))
resp2, err := client.Do(req)
- require.Nil(t, err)
+ require.NoError(t, err)
defer resp2.Body.Close()
h.server.Shutdown(context.Background())
@@ -221,29 +223,33 @@ func TestServe(t *testing.T) {
func TestClientClosesThenReconnects(t *testing.T) {
l := zap.NewNop()
- u, _ := url.Parse("bolt://test.db")
- bt, _ := NewTransport(u, l, nil)
+ u, err := url.Parse("bolt://test.db")
+ require.NoError(t, err)
+
+ bt, err := NewTransport(u, l)
+ require.NoError(t, err)
+ defer os.Remove("test.db")
+
h := createAnonymousDummy(WithLogger(l), WithTransport(bt))
transport := h.transport.(*BoltTransport)
- defer os.Remove("test.db")
go h.Serve()
// loop until the web server is ready
var resp *http.Response
client := http.Client{Timeout: 10 * time.Second}
for resp == nil {
- resp, _ = client.Get("http://" + testAddr + "/") //nolint:bodyclose
+ resp, _ = client.Get(testURLscheme + testAddr) //nolint:bodyclose
}
- resp.Body.Close()
+ require.NoError(t, resp.Body.Close())
var wg sync.WaitGroup
subscribe := func(expectedBodyData string) {
cx, cancel := context.WithCancel(context.Background())
- req, _ := http.NewRequest("GET", testURL+"?topic=http%3A%2F%2Fexample.com%2Ffoo%2F1", nil)
+ req, _ := http.NewRequest(http.MethodGet, testURL+"?topic=http%3A%2F%2Fexample.com%2Ffoo%2F1", nil)
req = req.WithContext(cx)
resp, err := http.DefaultClient.Do(req)
- require.Nil(t, err)
+ require.NoError(t, err)
var receivedBody strings.Builder
buf := make([]byte, 1024)
@@ -261,30 +267,30 @@ func TestClientClosesThenReconnects(t *testing.T) {
}
}
- resp.Body.Close()
+ require.NoError(t, resp.Body.Close())
wg.Done()
}
publish := func(data string, waitForSubscribers int) {
for {
- transport.Lock()
+ transport.RLock()
l := transport.subscribers.Len()
- transport.Unlock()
+ transport.RUnlock()
if l >= waitForSubscribers {
break
}
}
body := url.Values{"topic": {"http://example.com/foo/1"}, "data": {data}, "id": {data}}
- req, err := http.NewRequest("POST", testURL, strings.NewReader(body.Encode()))
- require.Nil(t, err)
+ req, err := http.NewRequest(http.MethodPost, testURL, strings.NewReader(body.Encode()))
+ require.NoError(t, err)
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
- req.Header.Add("Authorization", "Bearer "+createDummyAuthorizedJWT(h, rolePublisher, []string{}))
+ req.Header.Add("Authorization", bearerPrefix+createDummyAuthorizedJWT(rolePublisher, []string{"*"}))
resp, err := client.Do(req)
- require.Nil(t, err)
+ require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.StatusCode)
- resp.Body.Close()
+ require.NoError(t, resp.Body.Close())
wg.Done()
}
@@ -319,7 +325,7 @@ func TestClientClosesThenReconnects(t *testing.T) {
}
func TestServeAcme(t *testing.T) {
- dir, _ := ioutil.TempDir("", "cert")
+ dir, _ := os.MkdirTemp("", "cert")
defer os.RemoveAll(dir)
h := createAnonymousDummy(WithAllowedHosts([]string{"example.com"}))
@@ -329,7 +335,7 @@ func TestServeAcme(t *testing.T) {
go h.Serve()
client := &http.Client{
- CheckRedirect: func(req *http.Request, via []*http.Request) error {
+ CheckRedirect: func(_ *http.Request, _ []*http.Request) error {
return http.ErrUseLastResponse
},
}
@@ -344,7 +350,7 @@ func TestServeAcme(t *testing.T) {
resp.Body.Close()
resp, err := client.Get("http://0.0.0.0:8080/.well-known/acme-challenge/does-not-exists")
- assert.Nil(t, err)
+ require.NoError(t, err)
require.NotNil(t, resp)
defer resp.Body.Close()
@@ -356,12 +362,12 @@ func TestMetricsAccess(t *testing.T) {
server := newTestServer(t)
defer server.shutdown()
- resp, err := server.client.Get("http://" + testMetricsAddr + "/metrics")
- require.Nil(t, err)
+ resp, err := server.client.Get(testURLscheme + testMetricsAddr + metricsPath)
+ require.NoError(t, err)
defer resp.Body.Close()
- resp, err = server.client.Get("http://" + testMetricsAddr + "/healthz")
- require.Nil(t, err)
+ resp, err = server.client.Get(testURLscheme + testMetricsAddr + "/healthz")
+ require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, 200, resp.StatusCode)
@@ -383,7 +389,7 @@ func TestMetricsCollect(t *testing.T) {
body = url.Values{"topic": {"http://example.com/foo/1"}, "data": {"second hello"}, "id": {"second"}}
server.publish(body)
- server.assertMetric("mercure_subscribers 3")
+ server.assertMetric("mercure_subscribers_connected 3")
server.assertMetric("mercure_subscribers_total 4")
server.assertMetric("mercure_updates_total 2")
}
@@ -392,12 +398,12 @@ func TestMetricsVersionIsAccessible(t *testing.T) {
server := newTestServer(t)
defer server.shutdown()
- resp, err := server.client.Get("http://" + testMetricsAddr + "/metrics")
- assert.Nil(t, err)
+ resp, err := server.client.Get(testURLscheme + testMetricsAddr + metricsPath)
+ require.NoError(t, err)
defer resp.Body.Close()
- b, err := ioutil.ReadAll(resp.Body)
- assert.Nil(t, err)
+ b, err := io.ReadAll(resp.Body)
+ require.NoError(t, err)
pattern := "mercure_version_info{architecture=\".+\",built_at=\".*\",commit=\".*\",go_version=\".+\",os=\".+\",version=\"dev\"} 1"
assert.Regexp(t, regexp.MustCompile(pattern), string(b))
@@ -425,7 +431,7 @@ func newTestServer(t *testing.T) testServer {
var resp *http.Response
client := http.Client{Timeout: 100 * time.Millisecond}
for resp == nil {
- resp, _ = client.Get("http://" + testAddr + "/") //nolint:bodyclose
+ resp, _ = client.Get(testURLscheme + testAddr + "/") //nolint:bodyclose
}
defer resp.Body.Close()
@@ -456,7 +462,7 @@ func (s *testServer) newSubscriber(topic string, keepAlive bool) {
go func() {
defer s.wgTested.Done()
resp, err := s.client.Get(testURL + "?topic=" + url.QueryEscape(topic))
- require.Nil(s.t, err)
+ require.NoError(s.t, err)
defer resp.Body.Close()
s.wgConnected.Done()
@@ -467,12 +473,12 @@ func (s *testServer) newSubscriber(topic string, keepAlive bool) {
}
func (s *testServer) publish(body url.Values) {
- req, _ := http.NewRequest("POST", testURL, strings.NewReader(body.Encode()))
+ req, _ := http.NewRequest(http.MethodPost, testURL, strings.NewReader(body.Encode()))
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
- req.Header.Add("Authorization", "Bearer "+createDummyAuthorizedJWT(s.h, rolePublisher, []string{}))
+ req.Header.Add("Authorization", bearerPrefix+createDummyAuthorizedJWT(rolePublisher, []string{"*"}))
resp, err := s.client.Do(req)
- require.Nil(s.t, err)
+ require.NoError(s.t, err)
defer resp.Body.Close()
}
@@ -481,12 +487,12 @@ func (s *testServer) waitSubscribers() {
}
func (s *testServer) assertMetric(metric string) {
- resp, err := s.client.Get("http://" + testMetricsAddr + "/metrics")
- assert.Nil(s.t, err)
+ resp, err := s.client.Get(testURLscheme + testMetricsAddr + metricsPath)
+ require.NoError(s.t, err)
defer resp.Body.Close()
- b, err := ioutil.ReadAll(resp.Body)
- assert.Nil(s.t, err)
+ b, err := io.ReadAll(resp.Body)
+ require.NoError(s.t, err)
assert.Contains(s.t, string(b), metric)
}
diff --git a/spec/mercure.md b/spec/mercure.md
index 3b9f4bdb..705d53c6 100644
--- a/spec/mercure.md
+++ b/spec/mercure.md
@@ -27,9 +27,14 @@ organization = "Les-Tilleuls.coop"
.# Abstract
-Mercure is a protocol enabling the pushing of data updates to web browsers and other HTTP clients in
+Mercure provides a common publish-subscribe mechanism for public and private web resources.
+Mercure enables the pushing of any web content to web browsers and other clients in
a fast, reliable and battery-efficient way. It is especially useful for publishing real-time updates
-of resources served through web APIs to web and mobile apps.
+of resources served through sites and web APIs to web and mobile apps.
+
+Subscription requests are relayed through hubs, which validate and verify the request.
+When new or updated content becomes available, hubs check if subscribers are authorized to receive it
+then distribute it.
{mainmatter}
@@ -39,27 +44,21 @@ The keywords **MUST**, **MUST NOT**, **REQUIRED**, **SHALL**, **SHALL NOT**, **S
NOT**, **RECOMMENDED**, **MAY**, and **OPTIONAL**, when they appear in this document, are to be
interpreted as described in [@!RFC2119].
- * Topic: The unit to which one can subscribe to changes. The topic **SHOULD** be identified
+* Topic: The unit to which one can subscribe to changes. The topic **SHOULD** be identified
by an IRI [@!RFC3987]. Using an HTTPS [@!RFC7230] or HTTP [@!RFC7230] URI [@!RFC3986] is
**RECOMMENDED**.
-
- * Update: The message containing the updated version of the topic. An update can be marked as
+* Update: The message containing the updated version of the topic. An update can be marked as
private, consequently, it must be dispatched only to subscribers allowed to receive it.
-
- * Topic selector: An expression matching one or several topics.
-
- * Publisher: An owner of a topic. Notifies the hub when the topic feed has been updated. As in
+* Topic selector: An expression matching one or several topics.
+* Publisher: An owner of a topic. Notifies the hub when the topic feed has been updated. As in
almost all pubsub systems, the publisher is unaware of the subscribers, if any. Other pubsub
- systems might call the publisher the "source". Typically a website or a web API, but can also be
+ systems might call the publisher the "source". Typically a site or a web API, but can also be
a web browser.
-
- * Subscriber: A client application that subscribes to real-time updates of topics using topic
+* Subscriber: A client application that subscribes to real-time updates of topics using topic
selectors. Typically a web or a mobile application, but can also be a server.
-
- * Subscription: A topic selector used by a subscriber to receive updates. A single subscriber can
+* Subscription: A topic selector used by a subscriber to receive updates. A single subscriber can
have several subscriptions, when it provides several topic selectors.
-
- * Hub: A server that handles subscription requests and distributes the content to subscribers when
+* Hub: A server that handles subscription requests and distributes the content to subscribers when
the corresponding topics have been updated. Any hub **MAY** implement its own policies on who
can use it.
@@ -68,7 +67,6 @@ interpreted as described in [@!RFC2119].
The discovery mechanism aims at identifying at least 2 URLs.
1. The URL of one or more hubs designated by the publisher.
-
2. The canonical URL for the topic to which subscribers are expected to use for subscriptions.
The URL of the hub **MUST** be the "well-known" [@!RFC5785] fixed path `/.well-known/mercure`.
@@ -88,18 +86,16 @@ header). The target URL of these links **MUST** be a hub implementing the Mercur
The publisher **MAY** provide the following target attributes in the Link Headers:
- * `last-event-id`: the identifier of the last event dispatched by the publisher at the time of
+* `last-event-id`: the identifier of the last event dispatched by the publisher at the time of
the generation of this resource. If provided, it **MUST** be passed to the hub through a query
- parameter called `Last-Event-ID` and will be used to ensure that possible updates having been
+ parameter called `lastEventID` and will be used to ensure that possible updates having been
made between the resource generation by the server and the connection to the hub are not lost.
See (#reconciliation).
-
- * `content-type`: the content type of the updates that will be pushed by the hub. If omitted,
+* `content-type`: the content type of the updates that will be pushed by the hub. If omitted,
the subscriber **MUST** assume that the content type will be the same as that of the original
resource. Setting the `content-type` attribute is especially useful to hint that partial updates
will be pushed, using formats such as JSON Patch [@RFC6902] or JSON Merge Patch [@RFC7386].
-
- * `key-set`: the URL of the key set to use to decrypt updates, encoded in the JWK set format
+* `key-set`: the URL of the key set to use to decrypt updates, encoded in the JWK set format
(JSON Web Key Set) [@!RFC7517]. See (#encryption). As this key set will contain a secret
key, the publisher must ensure that only the subscriber can access to this URL. To do so, the
authorization mechanism (see (#authorization)) can be reused.
@@ -201,13 +197,10 @@ Note: URLs and IRIs are valid URI templates.
To determine if a string matches a selector, the following steps must be followed:
1. If the topic selector is `*` then the string matches the selector.
-
2. If the topic selector and the string are exactly the same, the string matches the selector. This
characteristic allows to compare a URI Template with another one.
-
3. If the topic selector is a valid URI Template, and that the string matches this URI Template,
the string matches the selector.
-
4. Otherwise the string does not match the selector.
# Subscription
@@ -292,28 +285,23 @@ it is able to do so. In this case, it **MAY NOT** implement the endpoint to publ
The request **MUST** be encoded using the `application/x-www-form-urlencoded` format
[@W3C.REC-html52-20171214] and contains the following name-value tuples:
- * `topic`: The identifiers of the updated topic. It is **RECOMMENDED** to use an IRI as
+* `topic`: The identifiers of the updated topic. It is **RECOMMENDED** to use an IRI as
identifier. If this name is present several times, the first occurrence is considered to be the
canonical IRI of the topic, and other ones are considered to be alternate IRIs. The hub **MUST**
dispatch this update to subscribers that are subscribed to both canonical or alternate IRIs.
-
- * `data` (optional): the content of the new version of this topic.
-
- * `private` (optional): if this name is set, the update **MUST NOT** be dispatched to subscribers
+* `data` (optional): the content of the new version of this topic.
+* `private` (optional): if this name is set, the update **MUST NOT** be dispatched to subscribers
not authorized to receive it. See (#authorization). It is recommended to set the value to `on`
but it **CAN** contain any value including an empty string.
-
- * `id` (optional): the topic's revision identifier: it will be used as the SSE's `id` property.
- The provided id **MUST NOT** start with the `#` character. The provided id **SHOULD** be a valid
+* `id` (optional): the topic's revision identifier: it will be used as the SSE's `id` property.
+ The provided ID **MUST NOT** start with the `#` character. The provided ID **SHOULD** be a valid
IRI. If omitted, the hub **MUST** generate a valid IRI [@!RFC3987]. An UUID [@RFC4122] or a
[DID](https://www.w3.org/TR/did-core/) **MAY** be used. Alternatively the hub **MAY** generate a
relative URI composed of a fragment (starting with `#`). This is convenient to return an offset
- or a sequence that is unique for this hub. Even if provided, the hub **MAY** ignore the id
- provided by the client and generate its own id.
-
- * `type` (optional): the SSE's `event` property (a specific event type).
-
- * `retry` (optional): the SSE's `retry` property (the reconnection time).
+ or a sequence that is unique for this hub. Even if provided, the hub **MAY** ignore the ID
+ provided by the client and generate its own ID.
+* `type` (optional): the SSE's `event` property (a specific event type).
+* `retry` (optional): the SSE's `retry` property (the reconnection time).
In the event of success, the HTTP response's body **MUST** be the `id` associated to this update
generated by the hub and a success HTTP status code **MUST** be returned. The publisher **MUST** be
@@ -342,16 +330,33 @@ To ensure that they are authorized, both publishers and subscribers must present
if the subscriber is a web browser. A different key **MAY** be used to sign subscribers' and
publishers' tokens.
-Two mechanisms are defined to present the JWS to the hub:
+Three mechanisms are defined to present the JWS to the hub:
+
+* using an `Authorization` HTTP header
+* using a cookie
+* using an `authorization` URI query parameter
+
+When using any authorization mechanism, the connection **MUST** use an encryption layer such as
+HTTPS.
+
+If an `Authorization` HTTP header is presented by the client, the JWS it contains **MUST** be used.
+The content of the `authorization` query parameter and of the cookie **MUST** be ignored.
- * using an `Authorization` HTTP header
+If an `authorization` query parameter is set by the client and no `Authorization` HTTP header is
+presented, the content of the query parameter **MUST** be used, the content of the cookie must be
+ignored.
- * using a cookie
+If the client tries to execute an operation it is not allowed to, a 403 HTTP status code **SHOULD**
+be returned.
+
+## Authorization HTTP Header
If the publisher or the subscriber is not a web browser, it **SHOULD** use an `Authorization`
-HTTP header. This `Authorization` header **MUST** contain the string `Bearer` followed by the JWS.
-The hub will check that the JWS conforms to the rules (defined later) ensuring that the client is
-authorized to publish or subscribe to updates.
+HTTP header. This `Authorization` header **MUST** contain the string `Bearer` followed by a space
+character and by the JWS. The hub will check that the JWS conforms to the rules (defined later)
+ensuring that the client is authorized to publish or subscribe to updates.
+
+## Cookie
By the `EventSource` specification [@W3C.REC-eventsource-20150203], web browsers
can not set custom HTTP headers for such connections, and they can only be
@@ -359,23 +364,45 @@ established using the `GET` HTTP method. However, cookies are supported and
can be included even in cross-domain requests if [the CORS credentials are
set](https://html.spec.whatwg.org/multipage/server-sent-events.html#dom-eventsourceinit-withcredentials):
-If the publisher or the subscriber is a web browser, it **SHOULD** send a cookie called
-`mercureAuthorization` containing the JWS when connecting to the hub.
+If the publisher or the subscriber is a web browser, it **SHOULD**, whenever possible, send a cookie
+containing the JWS when connecting to the hub.
+It is **RECOMMENDED** to name the cookie `mercureAuthorization`, but it may be necessary to use
+a different name to prevent conflicts when using multiple hubs on the same domain.
-Whenever possible, the `mercureAuthorization` cookie **SHOULD** be set during discovery (see
-(#discovery)) to improve the overall security. Consequently, if the cookie is set during the
-discovery, both the publisher and the hub have to share the same second level domain. The `Domain`
-attribute **MAY** be used to allow the publisher and the hub to use different subdomains. See
-(#discovery).
+The cookie **SHOULD** be set during discovery (see (#discovery)) to improve the overall security.
+Consequently, if the cookie is set during discovery, both the publisher and the hub have to share
+the same second level domain. The `Domain` attribute **MAY** be used to allow the publisher and the
+hub to use different subdomains. See (#discovery).
The cookie **SHOULD** have the `Secure`, `HttpOnly` and `SameSite` attributes set. The cookie's
`Path` attribute **SHOULD** also be set to the hub's URL. See (#security-considerations).
-When using authorization mechanisms, the connection **MUST** use an encryption layer such as HTTPS.
+## URI Query Parameter
+
+If it's not possible for the client to use an `Authorization` HTTP header nor a cookie, the JWS can
+be passed as a request URI query component as defined by "Uniform Resource Identifier (URI): Generic
+Syntax" [@!RFC3986], using the `authorization` parameter.
+
+The `authorization` query parameter **MUST** be properly separated from the `topic` parameter and
+from other request-specific parameters using `&` character(s) (ASCII code 38).
+
+For example, the client makes the following HTTP request using transport-layer security:
+
+~~~ http
+GET /.well-known/mercure?topic=https://example.com/books/foo&authorization= HTTP/1.1
+Host: hub.example.com
+~~~
+
+Clients using the URI Query Parameter method **SHOULD** also send a `Cache-Control` header
+containing the `no-store` option. Server success (2XX status) responses to these requests SHOULD
+contain a `Cache-Control` header with the `private` option.
+
+Because of the security weaknesses associated with the URI method (see (#security-considerations)),
+including the high likelihood that the URL containing the access token will be logged, it **SHOULD
+NOT** be used unless it is impossible to transport the access token in the `Authorization` request
+header field or in a secure cookie. Hubs **MAY** support this method.
-If both an `Authorization` HTTP header and a cookie named `mercureAuthorization` are presented by
-the client, the cookie **MUST** be ignored. If the client tries to execute an operation it is not
-allowed to, a 403 HTTP status code **SHOULD** be returned.
+This method is not recommended due to its security deficiencies.
## Publishers
@@ -386,13 +413,8 @@ To be allowed to publish an update, the JWS presented by the publisher **MUST**
called `mercure`, and this claim **MUST** contain a `publish` key. `mercure.publish` contains an
array of topic selectors. See (#topic-selectors).
-If `mercure.publish`:
-
- * is not defined, then the publisher **MUST NOT** be authorized to dispatch any update
-
- * contains an empty array, the publisher **MUST NOT** be authorized to publish private updates,
- but can publish public updates for all topics.
-
+If `mercure.publish` is not defined, or contains an empty array, then the publisher **MUST NOT**
+be authorized to dispatch any update.
Otherwise, the hub **MUST** check that every topics of the update to dispatch matches at least one
of the topic selectors contained in `mercure.publish`.
@@ -405,6 +427,9 @@ To receive updates marked as `private`, a subscriber **MUST** prove that it is a
least one of the topics of this update. If the subscriber is not authorized to receive an update
marked as `private`, it **MUST NOT** receive it.
+If the presented JWS contains an expiration time in the standard `exp` claim defined in [@!RFC7519],
+the connection **MUST** be closed by the hub at that time.
+
To receive updates marked as `private`, the JWS presented by the subscriber **MUST** have a
claim named `mercure` with a key named `subscribe` that contains an array of topic selectors. See
(#topic-selectors).
@@ -468,31 +493,33 @@ UUID [@RFC4122] or a [DID](https://www.w3.org/TR/did-core/) **MAY** be used.
According to the server-sent events specification, in case of connection
lost the subscriber will try to automatically re-connect. During the
-re-connection, the subscriber **MUST** send the last received event id in a
+re-connection, the subscriber **MUST** send the last received event ID in a
[Last-Event-ID](https://html.spec.whatwg.org/multipage/iana.html#last-event-id) HTTP header.
In order to fetch any update dispatched between the initial resource generation by the publisher and
-the connection to the hub, the subscriber **MUST** send the event id provided during the discovery
-as a `Last-Event-ID` header or query parameter. See (#discovery).
+the connection to the hub, the subscriber **MUST** send the event ID provided during the discovery
+as a `Last-Event-ID` header or a `lastEventID` query parameter. See (#discovery).
`EventSource` implementations may not allow to set HTTP headers during the first connection (before
a reconnection) and implementations in web browsers don't allow to set it.
-To work around this problem, the hub **MUST** also allow to pass the last event id in a query
-parameter named `Last-Event-ID`.
+To work around this problem, the hub **MUST** also allow to pass the last event ID in a query
+parameter named `lastEventID`.
-If both the `Last-Event-ID` HTTP header and the query parameter are present, the HTTP header
-**MUST** take precedence.
+If both the `Last-Event-ID` HTTP header and the `lastEventID` query parameter are present,
+the HTTP header **MUST** take precedence.
-If the `Last-Event-ID` HTTP header or query parameter exists, the hub **SHOULD** send all events
-published following the one bearing this identifier to the subscriber.
+If the `Last-Event-ID` HTTP header or the `lastEventID` query parameter exists,
+the hub **SHOULD** send all events published following the one bearing this identifier
+to the subscriber.
The reserved value `earliest` can be used to hint the hub to send all updates it has for the
subscribed topics. According to its own policy, the hub **MAY** or **MAY NOT** fulfil this request.
The hub **MAY** discard some events for operational reasons. When the request contains a
-`Last-Event-ID` HTTP header or query parameter the hub **MUST** set a `Last-Event-ID` header on
-the HTTP response. The value of the `Last-Event-ID` response header **MUST** be the id of the event
+`Last-Event-ID` HTTP header or a `lastEventID` query parameter the hub **MUST** set
+a `Last-Event-ID` header on the HTTP response.
+The value of the `Last-Event-ID` response header **MUST** be the ID of the event
preceding the first one sent to the subscriber, or the reserved value `earliest` if there is no
preceding event (it happens when the hub history is empty, when the subscriber requests the earliest
event or when the subscriber requests an event that doesn't exist).
@@ -503,7 +530,7 @@ partial updates in the JSON Patch [@RFC6902] format, or when using the hub as an
updates lost can cause data lost.
To detect if a data lost ocurred, the subscriber **CAN** compare the value of the `Last-Event-ID`
-response HTTP header with the `Last-Event-ID` it requested. In case of data lost, the subscriber
+response HTTP header with the last event ID it requested. In case of data lost, the subscriber
**SHOULD** re-fetch the original topic.
Note: Native `EventSource` implementations don't give access to headers associated with the HTTP
@@ -541,21 +568,15 @@ identifier. `{subscriber}` **SHOULD** be an IRI [@!RFC3987]. An UUID [@RFC4122]
The content of the update **MUST** be a JSON-LD [@!W3C.REC-json-ld-20140116] document containing at
least the following properties:
- * `@context`: the fixed value `https://mercure.rocks/`. `@context` can be omitted if already
+* `@context`: the fixed value `https://mercure.rocks/`. `@context` can be omitted if already
defined in a parent node. See (#json-ld-context).
-
- * `id`: the identifier of this update, it **MUST** be the same value as the subscription update's
+* `id`: the identifier of this update, it **MUST** be the same value as the subscription update's
topic
-
- * `type`: the fixed value `Subscription`
-
- * `topic`: the topic selector used of this subscription
-
- * `subscriber`: the topic identifier of the subscriber. It **SHOULD** be an IRI.
-
- * `active`: `true` when the subscription is active, and `false` when it is terminated
-
- * `payload` (optional): the content of `mercure.payload` in the subscriber's JWS (see
+* `type`: the fixed value `Subscription`
+* `topic`: the topic selector used of this subscription
+* `subscriber`: the topic identifier of the subscriber. It **SHOULD** be an IRI.
+* `active`: `true` when the subscription is active, and `false` when it is terminated
+* `payload` (optional): the content of `mercure.payload` in the subscriber's JWS (see
(#authorization))
The JSON-LD document **MAY** contain other properties.
@@ -587,12 +608,10 @@ date.
The web API **MUST** expose endpoints following these patterns:
- * `/.well-known/mercure/subscriptions`: the collection of subscriptions
-
- * `/.well-known/mercure/subscriptions/{topic}`: the collection of subscriptions for the given
+* `/.well-known/mercure/subscriptions`: the collection of subscriptions
+* `/.well-known/mercure/subscriptions/{topic}`: the collection of subscriptions for the given
topic selector
-
- * `/.well-known/mercure/subscriptions/{topic}/{subscriber}`: a specific subscription
+* `/.well-known/mercure/subscriptions/{topic}/{subscriber}`: a specific subscription
To access to the URLs exposed by the web API, clients **MUST** be authorized according to the rules
defined in (#authorization). The requested URL **MUST** match at least one of the topic selectors
@@ -612,19 +631,16 @@ them.
Collection endpoints **MUST** return JSON-LD documents containing at least the following properties:
- * `@context`: the fixed value `https://mercure.rocks/`. `@context` can be omitted if already
+* `@context`: the fixed value `https://mercure.rocks/`. `@context` can be omitted if already
defined in a parent node. See (#json-ld-context).
-
- * `id`: the URL used to retrieve the document
-
- * `type`: the fixed value `Subscriptions`
-
- * `subscriptions`: an array of subscription documents as described in (#subscription-events)
+* `id`: the URL used to retrieve the document
+* `type`: the fixed value `Subscriptions`
+* `subscriptions`: an array of subscription documents as described in (#subscription-events)
In addition, all endpoints **MUST** set the `lastEventID` property at the root of the returned
JSON-LD document:
- * `lastEventID`: the identifier of the last event dispatched by the hub at the time of this
+* `lastEventID`: the identifier of the last event dispatched by the hub at the time of this
request (see (#reconciliation)). The value **MUST** be `earliest` if no events have been
dispatched yet. The value of this property **SHOULD** be passed back to the hub when subscribing
to subscription events to prevent data loss.
@@ -782,35 +798,28 @@ relevant if the hub is managed by an external provider.
A new "well-known" URI as described in (#discovery) has been registered in the "Well-Known URIs"
registry as described below:
- * URI Suffix: mercure
-
- * Change Controller: IETF
-
- * Specification document(s): This specification, (#discovery)
-
- * Related information: N/A
+* URI Suffix: mercure
+* Change Controller: IETF
+* Specification document(s): This specification, (#discovery)
+* Related information: N/A
## Link Relation Types Registry
A new "Link Relation Type" as described in (#discovery) has been registered in the "Link Relation
Type" registry with the following entry:
- * Relation Name: mercure
-
- * Description: The Mercure Hub to use to subscribe to updates of this resource.
-
- * Reference: This specification, (#discovery)
+* Relation Name: mercure
+* Description: The Mercure Hub to use to subscribe to updates of this resource.
+* Reference: This specification, (#discovery)
## JSON Web Token (JWT) Registry
A new "JSON Web Token Claim" as described in (#authorization) **will be** registered in the "JSON
Web Token (JWT)" with the following entry:
- * Claim Name: mercure
-
- * Description: Mercure data.
-
- * Reference: This specification, (#authorization)
+* Claim Name: mercure
+* Description: Mercure data.
+* Reference: This specification, (#authorization)
# Security Considerations
@@ -846,6 +855,11 @@ CSRF prevention techniques, including those previously mentioned, are described
in depth in [OWASP's Cross-Site Request Forgery (CSRF) Prevention Cheat
Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html).
+JWSs **SHOULD NOT** be passed in page URLs (for example, using the `authorization` query string
+parameter). Browsers, web servers, and other software may not adequately secure URLs in the browser
+history, web server logs, and other data structures. If JWS are passed in page URLs, attackers might
+be able to steal them from the history data, logs, or other unsecured locations.
+
# Implementation Status
[RFC Editor Note: Please remove this entire section prior to publication as an RFC.]
@@ -906,6 +920,40 @@ Interoperability:
Reported compatible with all major browsers and server-side tools.
+## Freddie
+
+Implementation Name and Details:
+
+Freddie,
+
+Brief Description:
+
+Freddie is a PHP implementation of the Mercure Hub Specification.
+
+Level of Maturity:
+
+Stable.
+
+Coverage:
+
+All the features of the protocol except the subscription events.
+
+Version compatibility:
+
+The implementation follows the latest draft.
+
+Licensing:
+
+All code is covered under the GNU General Public License v3.0.
+
+Contact Information:
+
+
+
+Interoperability:
+
+Reported compatible with all major browsers and server-side tools.
+
## Ilshidur/node-mercure
Implementation Name and Details:
@@ -1062,7 +1110,7 @@ Reported compatible with the reference implementation of the Mercure Hub.
Implementation Name and Details:
-dart_mercure, available at
+dart*mercure, available at <*mercure>
Brief Description:
diff --git a/spec/openapi.yaml b/spec/openapi.yaml
index 275bb7fc..79b7ae08 100644
--- a/spec/openapi.yaml
+++ b/spec/openapi.yaml
@@ -56,26 +56,26 @@ paths:
"application/x-www-form-urlencoded":
schema:
properties:
- topic:
- description: IRIs of the updated topic. If this key is present several times, the first occurrence is considered to be the canonical URL of the topic, and other ones are considered to be alternate URLs.
- type: array
- items:
- type: string
- data:
- description: The content of the new version of this topic.
+ topic:
+ description: IRIs of the updated topic. If this key is present several times, the first occurrence is considered to be the canonical URL of the topic, and other ones are considered to be alternate URLs.
+ type: array
+ items:
type: string
- private:
- description: To mark an update as private. If not provided, this update will be public.
- type: boolean
- id:
- description: "The topic's revision identifier: it will be used as the SSE's `id` property."
- type: string
- type:
- description: The SSE's `event` property (a specific event type).
- type: string
- retry:
- description: The SSE's `retry` property (the reconnection time).
- type: integer
+ data:
+ description: The content of the new version of this topic.
+ type: string
+ private:
+ description: To mark an update as private. If not provided, this update will be public.
+ type: boolean
+ id:
+ description: "The topic's revision identifier: it will be used as the SSE's `id` property."
+ type: string
+ type:
+ description: The SSE's `event` property (a specific event type).
+ type: string
+ retry:
+ description: The SSE's `retry` property (the reconnection time).
+ type: integer
required:
- topic
- data
diff --git a/subscribe.go b/subscribe.go
index 2feee276..0b0cbab0 100644
--- a/subscribe.go
+++ b/subscribe.go
@@ -2,65 +2,146 @@ package mercure
import (
"encoding/json"
- "fmt"
- "io"
+ "errors"
"net/http"
"time"
"go.uber.org/zap"
- "go.uber.org/zap/zapcore"
)
+type responseController struct {
+ http.ResponseController
+ rw http.ResponseWriter
+ // disconnectionTime is the JWT expiration date minus hub.dispatchTimeout, or time.Now() plus hub.writeTimeout minus hub.dispatchTimeout
+ disconnectionTime time.Time
+ // writeDeadline is the JWT expiration date or time.Now() + hub.writeTimeout
+ writeDeadline time.Time
+ hub *Hub
+ subscriber *Subscriber
+}
+
+func (rc *responseController) setDispatchWriteDeadline() bool {
+ if rc.hub.dispatchTimeout == 0 {
+ return true
+ }
+
+ deadline := time.Now().Add(rc.hub.dispatchTimeout)
+ if deadline.After(rc.writeDeadline) {
+ return true
+ }
+
+ if err := rc.SetWriteDeadline(deadline); err != nil {
+ if c := rc.hub.logger.Check(zap.ErrorLevel, "Unable to set dispatch write deadline"); c != nil {
+ c.Write(zap.Object("subscriber", rc.subscriber), zap.Error(err))
+ }
+
+ return false
+ }
+
+ return true
+}
+
+func (rc *responseController) setDefaultWriteDeadline() bool {
+ if err := rc.SetWriteDeadline(rc.writeDeadline); err != nil {
+ if errors.Is(err, http.ErrNotSupported) {
+ panic(err)
+ }
+
+ if c := rc.hub.logger.Check(zap.InfoLevel, "Error while setting default write deadline"); c != nil {
+ c.Write(zap.Object("subscriber", rc.subscriber), zap.Error(err))
+ }
+
+ return false
+ }
+
+ return true
+}
+
+func (rc *responseController) flush() bool {
+ if err := rc.Flush(); err != nil {
+ if errors.Is(err, http.ErrNotSupported) {
+ panic(err)
+ }
+
+ if c := rc.hub.logger.Check(zap.InfoLevel, "Unable to flush"); c != nil {
+ c.Write(zap.Object("subscriber", rc.subscriber), zap.Error(err))
+ }
+
+ return false
+ }
+
+ return true
+}
+
+func (h *Hub) newResponseController(w http.ResponseWriter, s *Subscriber) *responseController {
+ wd := h.getWriteDeadline(s)
+
+ return &responseController{*http.NewResponseController(w), w, wd.Add(-h.dispatchTimeout), wd, h, s} // nolint:bodyclose
+}
+
+func (h *Hub) getWriteDeadline(s *Subscriber) (deadline time.Time) {
+ if h.writeTimeout != 0 {
+ deadline = time.Now().Add(h.writeTimeout)
+ }
+
+ if s.Claims != nil && s.Claims.ExpiresAt != nil && (deadline == time.Time{} || s.Claims.ExpiresAt.Time.Before(deadline)) {
+ deadline = s.Claims.ExpiresAt.Time
+ }
+
+ return
+}
+
// SubscribeHandler creates a keep alive connection and sends the events to the subscribers.
+//
+//nolint:funlen,gocognit
func (h *Hub) SubscribeHandler(w http.ResponseWriter, r *http.Request) {
- assertFlusher(w)
-
- s := h.registerSubscriber(w, r)
+ s, rc := h.registerSubscriber(w, r)
if s == nil {
return
}
defer h.shutdown(s)
- var heartbeatTimer *time.Timer
- var heartbeatTimerC <-chan time.Time
+ rc.setDefaultWriteDeadline()
+
+ var (
+ heartbeatTimer *time.Timer
+ heartbeatTimerC <-chan time.Time
+ disconnectionTimerC <-chan time.Time
+ )
+
if h.heartbeat != 0 {
heartbeatTimer = time.NewTimer(h.heartbeat)
defer heartbeatTimer.Stop()
heartbeatTimerC = heartbeatTimer.C
}
-
- var writeTimer *time.Timer
- var writeTimerC <-chan time.Time
if h.writeTimeout != 0 {
- writeTimer = time.NewTimer(h.writeTimeout - h.dispatchTimeout)
- defer writeTimer.Stop()
- writeTimerC = writeTimer.C
+ disconnectionTimer := time.NewTimer(time.Until(rc.disconnectionTime))
+ defer disconnectionTimer.Stop()
+ disconnectionTimerC = disconnectionTimer.C
}
for {
select {
case <-r.Context().Done():
- if c := h.logger.Check(zap.DebugLevel, "connection closed by the client"); c != nil {
- c.Write(zap.Object("subscriber", s))
- }
-
- return
- case <-writeTimerC:
- if c := h.logger.Check(zap.DebugLevel, "write timeout: close the connection"); c != nil {
+ if c := h.logger.Check(zap.DebugLevel, "Connection closed by the client"); c != nil {
c.Write(zap.Object("subscriber", s))
}
return
case <-heartbeatTimerC:
// Send a SSE comment as a heartbeat, to prevent issues with some proxies and old browsers
- if !h.write(w, s, ":\n") {
+ if !h.write(rc, ":\n") {
return
}
heartbeatTimer.Reset(h.heartbeat)
+ case <-disconnectionTimerC:
+ // Cleanly close the HTTP connection before the write deadline to prevent client-side errors
+ return
case update, ok := <-s.Receive():
h.logger.Info("Subscriber:received update")
- if !ok || !h.write(w, s, newSerializedUpdate(update).event) {
+ if !ok || !h.write(rc, newSerializedUpdate(update).event) {
h.logger.Info("Subscriber:received not ok")
+
return
}
if heartbeatTimer != nil {
@@ -69,7 +150,7 @@ func (h *Hub) SubscribeHandler(w http.ResponseWriter, r *http.Request) {
}
heartbeatTimer.Reset(h.heartbeat)
}
- if c := h.logger.Check(zap.InfoLevel, "Update sent"); c != nil {
+ if c := h.logger.Check(zap.DebugLevel, "Update sent"); c != nil {
c.Write(zap.Object("subscriber", s), zap.Object("update", update))
}
}
@@ -77,25 +158,27 @@ func (h *Hub) SubscribeHandler(w http.ResponseWriter, r *http.Request) {
}
// registerSubscriber initializes the connection.
-func (h *Hub) registerSubscriber(w http.ResponseWriter, r *http.Request) *Subscriber {
- s := NewSubscriber(retrieveLastEventID(r), h.logger)
+func (h *Hub) registerSubscriber(w http.ResponseWriter, r *http.Request) (*Subscriber, *responseController) {
+ s := NewSubscriber(retrieveLastEventID(r, h.opt, h.logger), h.logger)
s.Debug = h.debug
s.RemoteAddr = r.RemoteAddr
var privateTopics []string
+ var claims *claims
- if h.subscriberJWT != nil {
- claims, err := authorize(r, h.subscriberJWT, nil, h.cookieName)
+ if h.subscriberJWTKeyFunc != nil {
+ var err error
+ claims, err = authorize(r, h.subscriberJWTKeyFunc, nil, h.cookieName)
if claims != nil {
s.Claims = claims
privateTopics = claims.Mercure.Subscribe
}
if err != nil || (claims == nil && !h.anonymous) {
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
- if c := h.logger.Check(zap.InfoLevel, "Subscriber unauthorized"); c != nil {
+ if c := h.logger.Check(zap.DebugLevel, "Subscriber unauthorized"); c != nil {
c.Write(zap.Object("subscriber", s), zap.Error(err))
}
- return nil
+ return nil, nil
}
}
@@ -103,7 +186,7 @@ func (h *Hub) registerSubscriber(w http.ResponseWriter, r *http.Request) *Subscr
if len(topics) == 0 {
http.Error(w, "Missing \"topic\" parameter.", http.StatusBadRequest)
- return nil
+ return nil, nil
}
s.SetTopics(topics, privateTopics)
@@ -117,21 +200,28 @@ func (h *Hub) registerSubscriber(w http.ResponseWriter, r *http.Request) *Subscr
c.Write(zap.Object("subscriber", s), zap.Error(err))
}
- return nil
+ return nil, nil
}
- sendHeaders(w, s)
+ h.sendHeaders(w, s)
+ rc := h.newResponseController(w, s)
+ rc.flush()
if c := h.logger.Check(zap.InfoLevel, "New subscriber"); c != nil {
- c.Write(zap.Object("subscriber", s))
+ fields := []LogField{zap.Object("subscriber", s)}
+ if claims != nil && h.logger.Level() == zap.DebugLevel {
+ fields = append(fields, zap.Reflect("payload", claims.Mercure.Payload))
+ }
+
+ c.Write(fields...)
}
h.metrics.SubscriberConnected(s)
- return s
+ return s, rc
}
// sendHeaders sends correct HTTP headers to create a keep-alive connection.
-func sendHeaders(w http.ResponseWriter, s *Subscriber) {
+func (h *Hub) sendHeaders(w http.ResponseWriter, s *Subscriber) {
// Keep alive, useful only for HTTP 1 clients https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Keep-Alive
w.Header().Set("Connection", "keep-alive")
@@ -152,49 +242,51 @@ func sendHeaders(w http.ResponseWriter, s *Subscriber) {
// Write a comment in the body
// Go currently doesn't provide a better way to flush the headers
- fmt.Fprint(w, ":\n")
- w.(http.Flusher).Flush()
+ w.Write([]byte{':', '\n'})
}
// retrieveLastEventID extracts the Last-Event-ID from the corresponding HTTP header with a fallback on the query parameter.
-func retrieveLastEventID(r *http.Request) string {
+func retrieveLastEventID(r *http.Request, opt *opt, logger Logger) string {
if id := r.Header.Get("Last-Event-ID"); id != "" {
return id
}
- return r.URL.Query().Get("Last-Event-ID")
-}
+ query := r.URL.Query()
+ if id := query.Get("lastEventID"); id != "" {
+ return id
+ }
-// Write sends the given string to the client.
-// It returns false if the dispatch timed out.
-// The current write cannot be cancelled because of https://github.com/golang/go/issues/16100
-func (h *Hub) write(w io.Writer, s zapcore.ObjectMarshaler, data string) bool {
- if h.dispatchTimeout == 0 {
- fmt.Fprint(w, data)
- w.(http.Flusher).Flush()
+ if legacyEventIDValues, present := query["Last-Event-ID"]; present {
+ if opt.isBackwardCompatiblyEnabledWith(7) {
+ logger.Info("Deprecated: the 'Last-Event-ID' query parameter is deprecated since the version 8 of the protocol, use 'lastEventID' instead.")
- return true
+ if len(legacyEventIDValues) != 0 {
+ return legacyEventIDValues[0]
+ }
+ } else {
+ logger.Info("Unsupported: the 'Last-Event-ID' query parameter is not supported anymore, use 'lastEventID' instead or enable backward compatibility with version 7 of the protocol.")
+ }
}
- done := make(chan struct{})
- go func() {
- fmt.Fprint(w, data)
- w.(http.Flusher).Flush()
- close(done)
- }()
+ return ""
+}
+
+// Write sends the given string to the client.
+// It returns false if the subscriber has been disconnected (e.g. timeout).
+func (h *Hub) write(rc *responseController, data string) bool {
+ if !rc.setDispatchWriteDeadline() {
+ return false
+ }
- timeout := time.NewTimer(h.dispatchTimeout)
- defer timeout.Stop()
- select {
- case <-done:
- return true
- case <-timeout.C:
- if c := h.logger.Check(zap.WarnLevel, "Dispatch timeout reached"); c != nil {
- c.Write(zap.Object("subscriber", s))
+ if _, err := rc.rw.Write([]byte(data)); err != nil {
+ if c := h.logger.Check(zap.DebugLevel, "Error writing to client"); c != nil {
+ c.Write(zap.Object("subscriber", rc.subscriber), zap.Error(err))
}
return false
}
+
+ return rc.flush() && rc.setDefaultWriteDeadline()
}
func (h *Hub) shutdown(s *Subscriber) {
@@ -228,9 +320,3 @@ func (h *Hub) dispatchSubscriptionUpdate(s *Subscriber, active bool) {
h.transport.Dispatch(u)
}
}
-
-func assertFlusher(w http.ResponseWriter) {
- if _, ok := w.(http.Flusher); !ok {
- panic("http.ResponseWriter must be an instance of http.Flusher")
- }
-}
diff --git a/subscribe_test.go b/subscribe_test.go
index 9a2acff0..ba39c2f3 100644
--- a/subscribe_test.go
+++ b/subscribe_test.go
@@ -3,7 +3,7 @@ package mercure
import (
"context"
"errors"
- "io/ioutil"
+ "io"
"net/http"
"net/http/httptest"
"os"
@@ -12,7 +12,12 @@ import (
"testing"
"time"
+ "github.com/golang-jwt/jwt/v5"
"github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "go.uber.org/zap"
+ "go.uber.org/zap/zapcore"
+ "go.uber.org/zap/zaptest/observer"
)
type responseWriterMock struct{}
@@ -25,7 +30,7 @@ func (m *responseWriterMock) Write([]byte) (int, error) {
return 0, nil
}
-func (m *responseWriterMock) WriteHeader(statusCode int) {
+func (m *responseWriterMock) WriteHeader(_ int) {
}
type responseTester struct {
@@ -67,20 +72,84 @@ func (rt *responseTester) WriteHeader(statusCode int) {
func (rt *responseTester) Flush() {
}
+func (rt *responseTester) SetWriteDeadline(_ time.Time) error {
+ return nil
+}
+
+type subscribeRecorder struct {
+ *httptest.ResponseRecorder
+ writeDeadline time.Time
+}
+
+func newSubscribeRecorder() *subscribeRecorder {
+ return &subscribeRecorder{ResponseRecorder: httptest.NewRecorder()}
+}
+
+func (r *subscribeRecorder) SetWriteDeadline(deadline time.Time) error {
+ if deadline.After(r.writeDeadline) {
+ r.writeDeadline = deadline
+ }
+
+ return nil
+}
+
+func (r *subscribeRecorder) Write(buf []byte) (int, error) {
+ if time.Now().After(r.writeDeadline) {
+ return 0, os.ErrDeadlineExceeded
+ }
+
+ return r.ResponseRecorder.Write(buf)
+}
+
+func (r *subscribeRecorder) WriteString(str string) (int, error) {
+ if time.Now().After(r.writeDeadline) {
+ return 0, os.ErrDeadlineExceeded
+ }
+
+ return r.ResponseRecorder.WriteString(str)
+}
+
+func (r *subscribeRecorder) FlushError() error {
+ if time.Now().After(r.writeDeadline) {
+ return os.ErrDeadlineExceeded
+ }
+
+ r.ResponseRecorder.Flush()
+
+ return nil
+}
+
func TestSubscribeNotAFlusher(t *testing.T) {
- hub := createDummy()
+ hub := createAnonymousDummy()
- req := httptest.NewRequest("GET", defaultHubURL, nil)
+ go func() {
+ s := hub.transport.(*LocalTransport)
+ var ready bool
+
+ for !ready {
+ s.RLock()
+ ready = s.subscribers.Len() != 0
+ s.RUnlock()
+ }
+
+ hub.transport.Dispatch(&Update{
+ Topics: []string{"http://example.com/foo"},
+ Event: Event{Data: "Hello World"},
+ })
+ }()
- assert.PanicsWithValue(t, "http.ResponseWriter must be an instance of http.Flusher", func() {
- hub.SubscribeHandler(&responseWriterMock{}, req)
+ assert.Panics(t, func() {
+ hub.SubscribeHandler(
+ &responseWriterMock{},
+ httptest.NewRequest(http.MethodGet, defaultHubURL+"?topic=http://example.com/foo", nil),
+ )
})
}
func TestSubscribeNoCookie(t *testing.T) {
hub := createDummy()
- req := httptest.NewRequest("GET", defaultHubURL, nil)
+ req := httptest.NewRequest(http.MethodGet, defaultHubURL, nil)
w := httptest.NewRecorder()
hub.SubscribeHandler(w, req)
@@ -95,7 +164,7 @@ func TestSubscribeNoCookie(t *testing.T) {
func TestSubscribeInvalidJWT(t *testing.T) {
hub := createDummy()
- req := httptest.NewRequest("GET", defaultHubURL, nil)
+ req := httptest.NewRequest(http.MethodGet, defaultHubURL, nil)
w := httptest.NewRecorder()
req.AddCookie(&http.Cookie{Name: "mercureAuthorization", Value: "invalid"})
@@ -111,7 +180,7 @@ func TestSubscribeInvalidJWT(t *testing.T) {
func TestSubscribeUnauthorizedJWT(t *testing.T) {
hub := createDummy()
- req := httptest.NewRequest("GET", defaultHubURL, nil)
+ req := httptest.NewRequest(http.MethodGet, defaultHubURL, nil)
w := httptest.NewRecorder()
req.AddCookie(&http.Cookie{Name: "mercureAuthorization", Value: createDummyUnauthorizedJWT()})
req.Header = http.Header{"Cookie": []string{w.Header().Get("Set-Cookie")}}
@@ -128,7 +197,7 @@ func TestSubscribeUnauthorizedJWT(t *testing.T) {
func TestSubscribeInvalidAlgJWT(t *testing.T) {
hub := createDummy()
- req := httptest.NewRequest("GET", defaultHubURL, nil)
+ req := httptest.NewRequest(http.MethodGet, defaultHubURL, nil)
w := httptest.NewRecorder()
req.AddCookie(&http.Cookie{Name: "mercureAuthorization", Value: createDummyNoneSignedJWT()})
@@ -144,7 +213,7 @@ func TestSubscribeInvalidAlgJWT(t *testing.T) {
func TestSubscribeNoTopic(t *testing.T) {
hub := createAnonymousDummy()
- req := httptest.NewRequest("GET", defaultHubURL, nil)
+ req := httptest.NewRequest(http.MethodGet, defaultHubURL, nil)
w := httptest.NewRecorder()
hub.SubscribeHandler(w, req)
@@ -182,7 +251,7 @@ func (*addSubscriberErrorTransport) Close() error {
func TestSubscribeAddSubscriberError(t *testing.T) {
hub := createAnonymousDummy(WithTransport(&addSubscriberErrorTransport{}))
- req := httptest.NewRequest("GET", defaultHubURL+"?topic=foo", nil)
+ req := httptest.NewRequest(http.MethodGet, defaultHubURL+"?topic=foo", nil)
w := httptest.NewRecorder()
hub.SubscribeHandler(w, req)
@@ -238,7 +307,7 @@ func testSubscribe(h interface{ Helper() }, numberOfSubscribers int) {
go func() {
defer wg.Done()
ctx, cancel := context.WithCancel(context.Background())
- req := httptest.NewRequest("GET", defaultHubURL+"?topic=http://example.com/books/1&topic=string&topic=http://example.com/reviews/{id}&topic=http://example.com/hub?topic=faulty{iri", nil).WithContext(ctx)
+ req := httptest.NewRequest(http.MethodGet, defaultHubURL+"?topic=http://example.com/books/1&topic=string&topic=http://example.com/reviews/{id}&topic=http://example.com/hub?topic=faulty{iri", nil).WithContext(ctx)
w := &responseTester{
expectedStatusCode: http.StatusOK,
@@ -257,6 +326,72 @@ func TestSubscribe(t *testing.T) {
testSubscribe(t, 3)
}
+func testSubscribeLogs(t *testing.T, hub *Hub, payload interface{}) {
+ t.Helper()
+
+ ctx, cancel := context.WithCancel(context.Background())
+ req := httptest.NewRequest(http.MethodGet, defaultHubURL+"?topic=http://example.com/reviews/{id}", nil).WithContext(ctx)
+ req.AddCookie(&http.Cookie{Name: "mercureAuthorization", Value: createDummyAuthorizedJWTWithPayload(roleSubscriber, []string{"http://example.com/reviews/22"}, payload)})
+
+ w := &responseTester{
+ expectedStatusCode: http.StatusOK,
+ expectedBody: ":\n",
+ t: t,
+ cancel: cancel,
+ }
+
+ hub.SubscribeHandler(w, req)
+}
+
+func TestSubscribeWithLogLevelDebug(t *testing.T) {
+ core, logs := observer.New(zapcore.DebugLevel)
+ payload := map[string]interface{}{
+ "bar": "baz",
+ "foo": "bar",
+ }
+
+ testSubscribeLogs(t, createDummy(
+ WithLogger(zap.New(core)),
+ ), payload)
+
+ assert.Equal(t, 1, logs.FilterMessage("New subscriber").FilterField(
+ zap.Reflect("payload", payload)).Len(),
+ )
+}
+
+func TestSubscribeLogLevelInfo(t *testing.T) {
+ core, logs := observer.New(zapcore.InfoLevel)
+ payload := map[string]interface{}{
+ "bar": "baz",
+ "foo": "bar",
+ }
+ testSubscribeLogs(t, createDummy(
+ WithLogger(zap.New(core)),
+ ), payload)
+
+ assert.Equal(t, 0, logs.FilterMessage("New subscriber").FilterFieldKey("payload").Len())
+}
+
+func TestSubscribeLogAnonymousSubscriber(t *testing.T) {
+ core, logs := observer.New(zapcore.DebugLevel)
+
+ h := createAnonymousDummy(WithLogger(zap.New(core)))
+
+ ctx, cancel := context.WithCancel(context.Background())
+ req := httptest.NewRequest(http.MethodGet, defaultHubURL+"?topic=http://example.com/", nil).WithContext(ctx)
+
+ w := &responseTester{
+ expectedStatusCode: http.StatusOK,
+ expectedBody: ":\n",
+ t: t,
+ cancel: cancel,
+ }
+
+ h.SubscribeHandler(w, req)
+
+ assert.Equal(t, 0, logs.FilterMessage("New subscriber").FilterFieldKey("payload").Len())
+}
+
func TestUnsubscribe(t *testing.T) {
hub := createAnonymousDummy()
@@ -268,8 +403,8 @@ func TestUnsubscribe(t *testing.T) {
wg.Add(1)
go func() {
defer wg.Done()
- req := httptest.NewRequest("GET", defaultHubURL+"?topic=http://example.com/books/1", nil).WithContext(ctx)
- hub.SubscribeHandler(httptest.NewRecorder(), req)
+ req := httptest.NewRequest(http.MethodGet, defaultHubURL+"?topic=http://example.com/books/1", nil).WithContext(ctx)
+ hub.SubscribeHandler(newSubscribeRecorder(), req)
assert.Equal(t, 0, s.subscribers.Len())
s.subscribers.Walk(0, func(s *Subscriber) bool {
_, ok := <-s.out
@@ -327,8 +462,8 @@ func TestSubscribePrivate(t *testing.T) {
}()
ctx, cancel := context.WithCancel(context.Background())
- req := httptest.NewRequest("GET", defaultHubURL+"?topic=http://example.com/reviews/{id}", nil).WithContext(ctx)
- req.AddCookie(&http.Cookie{Name: "mercureAuthorization", Value: createDummyAuthorizedJWT(hub, roleSubscriber, []string{"http://example.com/reviews/22", "http://example.com/reviews/23"})})
+ req := httptest.NewRequest(http.MethodGet, defaultHubURL+"?topic=http://example.com/reviews/{id}", nil).WithContext(ctx)
+ req.AddCookie(&http.Cookie{Name: "mercureAuthorization", Value: createDummyAuthorizedJWT(roleSubscriber, []string{"http://example.com/reviews/22", "http://example.com/reviews/23"})})
w := &responseTester{
expectedStatusCode: http.StatusOK,
@@ -350,14 +485,14 @@ func TestSubscriptionEvents(t *testing.T) {
go func() {
// Authorized to receive connection events
defer wg.Done()
- req := httptest.NewRequest("GET", defaultHubURL+"?topic=/.well-known/mercure/subscriptions/{topic}/{subscriber}", nil).WithContext(ctx1)
- req.AddCookie(&http.Cookie{Name: "mercureAuthorization", Value: createDummyAuthorizedJWT(hub, roleSubscriber, []string{"/.well-known/mercure/subscriptions/{topic}/{subscriber}"})})
- w := httptest.NewRecorder()
+ req := httptest.NewRequest(http.MethodGet, defaultHubURL+"?topic=/.well-known/mercure/subscriptions/{topic}/{subscriber}", nil).WithContext(ctx1)
+ req.AddCookie(&http.Cookie{Name: "mercureAuthorization", Value: createDummyAuthorizedJWT(roleSubscriber, []string{"/.well-known/mercure/subscriptions/{topic}/{subscriber}"})})
+ w := newSubscribeRecorder()
hub.SubscribeHandler(w, req)
resp := w.Result()
defer resp.Body.Close()
- body, _ := ioutil.ReadAll(resp.Body)
+ body, _ := io.ReadAll(resp.Body)
assert.Equal(t, http.StatusOK, resp.StatusCode)
bodyContent := string(body)
@@ -375,17 +510,17 @@ func TestSubscriptionEvents(t *testing.T) {
go func() {
// Not authorized to receive connection events
defer wg.Done()
- req := httptest.NewRequest("GET", defaultHubURL+"?topic=/.well-known/mercure/subscriptions/{topicSelector}/{subscriber}", nil).WithContext(ctx2)
- req.AddCookie(&http.Cookie{Name: "mercureAuthorization", Value: createDummyAuthorizedJWT(hub, roleSubscriber, []string{})})
- w := httptest.NewRecorder()
+ req := httptest.NewRequest(http.MethodGet, defaultHubURL+"?topic=/.well-known/mercure/subscriptions/{topicSelector}/{subscriber}", nil).WithContext(ctx2)
+ req.AddCookie(&http.Cookie{Name: "mercureAuthorization", Value: createDummyAuthorizedJWT(roleSubscriber, []string{})})
+ w := newSubscribeRecorder()
hub.SubscribeHandler(w, req)
resp := w.Result()
defer resp.Body.Close()
- body, _ := ioutil.ReadAll(resp.Body)
+ body, _ := io.ReadAll(resp.Body)
assert.Equal(t, http.StatusOK, resp.StatusCode)
- assert.Equal(t, ":\n", string(body))
+ assert.Equal(t, "", string(body))
}()
go func() {
@@ -399,8 +534,8 @@ func TestSubscriptionEvents(t *testing.T) {
}
ctx, cancelRequest2 := context.WithCancel(context.Background())
- req := httptest.NewRequest("GET", defaultHubURL+"?topic=https://example.com", nil).WithContext(ctx)
- req.AddCookie(&http.Cookie{Name: "mercureAuthorization", Value: createDummyAuthorizedJWT(hub, roleSubscriber, []string{})})
+ req := httptest.NewRequest(http.MethodGet, defaultHubURL+"?topic=https://example.com", nil).WithContext(ctx)
+ req.AddCookie(&http.Cookie{Name: "mercureAuthorization", Value: createDummyAuthorizedJWT(roleSubscriber, []string{})})
w := &responseTester{
expectedStatusCode: http.StatusOK,
@@ -447,8 +582,8 @@ func TestSubscribeAll(t *testing.T) {
}()
ctx, cancel := context.WithCancel(context.Background())
- req := httptest.NewRequest("GET", defaultHubURL+"?topic=http://example.com/reviews/{id}", nil).WithContext(ctx)
- req.Header.Add("Authorization", "Bearer "+createDummyAuthorizedJWT(hub, roleSubscriber, []string{"random", "*"}))
+ req := httptest.NewRequest(http.MethodGet, defaultHubURL+"?topic=http://example.com/reviews/{id}", nil).WithContext(ctx)
+ req.Header.Add("Authorization", bearerPrefix+createDummyAuthorizedJWT(roleSubscriber, []string{"random", "*"}))
w := &responseTester{
expectedStatusCode: http.StatusOK,
@@ -462,7 +597,7 @@ func TestSubscribeAll(t *testing.T) {
func TestSendMissedEvents(t *testing.T) {
bt := createBoltTransport("bolt://test.db")
- hub := createAnonymousDummy(WithLogger(bt.logger), WithTransport(bt))
+ hub := createAnonymousDummy(WithLogger(bt.logger), WithTransport(bt), WithProtocolVersionCompatibility(7))
transport := hub.transport.(*BoltTransport)
defer transport.Close()
defer os.Remove("test.db")
@@ -483,13 +618,30 @@ func TestSendMissedEvents(t *testing.T) {
})
var wg sync.WaitGroup
- wg.Add(2)
+ wg.Add(3)
+
+ // Using deprecated 'Last-Event-ID' query parameter
+ go func() {
+ defer wg.Done()
+
+ ctx, cancel := context.WithCancel(context.Background())
+ req := httptest.NewRequest(http.MethodGet, defaultHubURL+"?topic=http://example.com/foos/{id}&Last-Event-ID=a", nil).WithContext(ctx)
+
+ w := &responseTester{
+ expectedStatusCode: http.StatusOK,
+ expectedBody: ":\nid: b\ndata: d2\n\n",
+ t: t,
+ cancel: cancel,
+ }
+
+ hub.SubscribeHandler(w, req)
+ }()
go func() {
defer wg.Done()
ctx, cancel := context.WithCancel(context.Background())
- req := httptest.NewRequest("GET", defaultHubURL+"?topic=http://example.com/foos/{id}&Last-Event-ID=a", nil).WithContext(ctx)
+ req := httptest.NewRequest(http.MethodGet, defaultHubURL+"?topic=http://example.com/foos/{id}&lastEventID=a", nil).WithContext(ctx)
w := &responseTester{
expectedStatusCode: http.StatusOK,
@@ -505,7 +657,7 @@ func TestSendMissedEvents(t *testing.T) {
defer wg.Done()
ctx, cancel := context.WithCancel(context.Background())
- req := httptest.NewRequest("GET", defaultHubURL+"?topic=http://example.com/foos/{id}", nil).WithContext(ctx)
+ req := httptest.NewRequest(http.MethodGet, defaultHubURL+"?topic=http://example.com/foos/{id}", nil).WithContext(ctx)
req.Header.Add("Last-Event-ID", "a")
w := &responseTester{
@@ -550,7 +702,7 @@ func TestSendAllEvents(t *testing.T) {
defer wg.Done()
ctx, cancel := context.WithCancel(context.Background())
- req := httptest.NewRequest("GET", defaultHubURL+"?topic=http://example.com/foos/{id}&Last-Event-ID="+EarliestLastEventID, nil).WithContext(ctx)
+ req := httptest.NewRequest(http.MethodGet, defaultHubURL+"?topic=http://example.com/foos/{id}&lastEventID="+EarliestLastEventID, nil).WithContext(ctx)
w := &responseTester{
header: http.Header{},
@@ -567,7 +719,7 @@ func TestSendAllEvents(t *testing.T) {
defer wg.Done()
ctx, cancel := context.WithCancel(context.Background())
- req := httptest.NewRequest("GET", defaultHubURL+"?topic=http://example.com/foos/{id}", nil).WithContext(ctx)
+ req := httptest.NewRequest(http.MethodGet, defaultHubURL+"?topic=http://example.com/foos/{id}", nil).WithContext(ctx)
req.Header.Add("Last-Event-ID", EarliestLastEventID)
w := &responseTester{
@@ -606,7 +758,7 @@ func TestUnknownLastEventID(t *testing.T) {
defer wg.Done()
ctx, cancel := context.WithCancel(context.Background())
- req := httptest.NewRequest("GET", defaultHubURL+"?topic=http://example.com/foos/{id}&Last-Event-ID=unknown", nil).WithContext(ctx)
+ req := httptest.NewRequest(http.MethodGet, defaultHubURL+"?topic=http://example.com/foos/{id}&lastEventID=unknown", nil).WithContext(ctx)
w := &responseTester{
header: http.Header{},
@@ -624,7 +776,7 @@ func TestUnknownLastEventID(t *testing.T) {
defer wg.Done()
ctx, cancel := context.WithCancel(context.Background())
- req := httptest.NewRequest("GET", defaultHubURL+"?topic=http://example.com/foos/{id}", nil).WithContext(ctx)
+ req := httptest.NewRequest(http.MethodGet, defaultHubURL+"?topic=http://example.com/foos/{id}", nil).WithContext(ctx)
req.Header.Add("Last-Event-ID", "unknown")
w := &responseTester{
@@ -674,7 +826,7 @@ func TestUnknownLastEventIDEmptyHistory(t *testing.T) {
defer wg.Done()
ctx, cancel := context.WithCancel(context.Background())
- req := httptest.NewRequest("GET", defaultHubURL+"?topic=http://example.com/foos/{id}&Last-Event-ID=unknown", nil).WithContext(ctx)
+ req := httptest.NewRequest(http.MethodGet, defaultHubURL+"?topic=http://example.com/foos/{id}&lastEventID=unknown", nil).WithContext(ctx)
w := &responseTester{
header: http.Header{},
@@ -692,7 +844,7 @@ func TestUnknownLastEventIDEmptyHistory(t *testing.T) {
defer wg.Done()
ctx, cancel := context.WithCancel(context.Background())
- req := httptest.NewRequest("GET", defaultHubURL+"?topic=http://example.com/foos/{id}", nil).WithContext(ctx)
+ req := httptest.NewRequest(http.MethodGet, defaultHubURL+"?topic=http://example.com/foos/{id}", nil).WithContext(ctx)
req.Header.Add("Last-Event-ID", "unknown")
w := &responseTester{
@@ -752,7 +904,7 @@ func TestSubscribeHeartbeat(t *testing.T) {
}()
ctx, cancel := context.WithCancel(context.Background())
- req := httptest.NewRequest("GET", defaultHubURL+"?topic=http://example.com/books/1&topic=http://example.com/reviews/{id}", nil).WithContext(ctx)
+ req := httptest.NewRequest(http.MethodGet, defaultHubURL+"?topic=http://example.com/books/1&topic=http://example.com/reviews/{id}", nil).WithContext(ctx)
w := &responseTester{
expectedStatusCode: http.StatusOK,
@@ -764,6 +916,33 @@ func TestSubscribeHeartbeat(t *testing.T) {
hub.SubscribeHandler(w, req)
}
+func TestSubscribeExpires(t *testing.T) {
+ hub := createAnonymousDummy(WithWriteTimeout(0), WithDispatchTimeout(0), WithHeartbeat(500*time.Millisecond))
+ token := jwt.New(jwt.SigningMethodHS256)
+
+ token.Claims = &claims{
+ Mercure: mercureClaim{
+ Subscribe: []string{"*"},
+ },
+ RegisteredClaims: jwt.RegisteredClaims{ExpiresAt: jwt.NewNumericDate(time.Now().Add(1 * time.Second))},
+ }
+
+ jwt, err := token.SignedString([]byte("subscriber"))
+ require.NoError(t, err)
+
+ req := httptest.NewRequest(http.MethodGet, defaultHubURL+"?topic=foo", nil)
+ req.Header.Add("Authorization", bearerPrefix+jwt)
+
+ w := newSubscribeRecorder()
+ hub.SubscribeHandler(w, req)
+
+ resp := w.Result()
+ defer resp.Body.Close()
+
+ assert.Equal(t, 200, resp.StatusCode)
+ assert.True(t, time.Now().After(token.Claims.(*claims).ExpiresAt.Time))
+}
+
func BenchmarkSubscribe(b *testing.B) {
for n := 0; n < b.N; n++ {
testSubscribe(b, 1000)
diff --git a/subscriber.go b/subscriber.go
index 1a6e1289..8edade9f 100644
--- a/subscriber.go
+++ b/subscriber.go
@@ -15,17 +15,17 @@ import (
// Subscriber represents a client subscribed to a list of topics.
type Subscriber struct {
- ID string
- EscapedID string
- Claims *claims
- EscapedTopics []string
- RequestLastEventID string
- RemoteAddr string
- Topics []string
- TopicRegexps []*regexp.Regexp
- PrivateTopics []string
- PrivateRegexps []*regexp.Regexp
- Debug bool
+ ID string
+ EscapedID string
+ Claims *claims
+ EscapedTopics []string
+ RequestLastEventID string
+ RemoteAddr string
+ SubscribedTopics []string
+ SubscribedTopicRegexps []*regexp.Regexp
+ AllowedPrivateTopics []string
+ AllowedPrivateRegexps []*regexp.Regexp
+ Debug bool
disconnected int32
out chan *Update
@@ -37,6 +37,8 @@ type Subscriber struct {
liveMutex sync.RWMutex
}
+const outBufferLength = 1000
+
// NewSubscriber creates a new subscriber.
func NewSubscriber(lastEventID string, logger Logger) *Subscriber {
id := "urn:uuid:" + uuid.Must(uuid.NewV4()).String()
@@ -45,7 +47,7 @@ func NewSubscriber(lastEventID string, logger Logger) *Subscriber {
EscapedID: url.QueryEscape(id),
RequestLastEventID: lastEventID,
responseLastEventID: make(chan string, 1),
- out: make(chan *Update, 1000),
+ out: make(chan *Update, outBufferLength),
logger: logger,
}
@@ -72,15 +74,17 @@ func (s *Subscriber) Dispatch(u *Update, fromHistory bool) bool {
}
s.outMutex.Lock()
- defer s.outMutex.Unlock()
if atomic.LoadInt32(&s.disconnected) > 0 {
+ s.outMutex.Unlock()
+
return false
}
select {
case s.out <- u:
+ s.outMutex.Unlock()
default:
- s.logger.Error("error when dispatching the update", zap.Any("update", u), zap.Any("subscriber", s))
+ s.handleFullChan()
return false
}
@@ -89,18 +93,26 @@ func (s *Subscriber) Dispatch(u *Update, fromHistory bool) bool {
}
// Ready flips the ready flag to true and flushes queued live updates returning number of events flushed.
-func (s *Subscriber) Ready() int {
+func (s *Subscriber) Ready() (n int) {
s.liveMutex.Lock()
- defer s.liveMutex.Unlock()
s.outMutex.Lock()
- defer s.outMutex.Unlock()
- n := len(s.liveQueue)
for _, u := range s.liveQueue {
- s.out <- u
+ select {
+ case s.out <- u:
+ n++
+ default:
+ s.handleFullChan()
+ s.liveMutex.Unlock()
+
+ return n
+ }
}
atomic.StoreInt32(&s.ready, 1)
+ s.outMutex.Unlock()
+ s.liveMutex.Unlock()
+
return n
}
@@ -128,26 +140,26 @@ func (s *Subscriber) Disconnect() {
}
// SetTopics compiles topic selector regexps.
-func (s *Subscriber) SetTopics(topics, privateTopics []string) {
- s.Topics = topics
- s.TopicRegexps = make([]*regexp.Regexp, len(topics))
- for i, ts := range topics {
+func (s *Subscriber) SetTopics(subscribedTopics, allowedPrivateTopics []string) {
+ s.SubscribedTopics = subscribedTopics
+ s.SubscribedTopicRegexps = make([]*regexp.Regexp, len(subscribedTopics))
+ for i, ts := range subscribedTopics {
var r *regexp.Regexp
if tpl, err := uritemplate.New(ts); err == nil {
r = tpl.Regexp()
}
- s.TopicRegexps[i] = r
+ s.SubscribedTopicRegexps[i] = r
}
- s.PrivateTopics = privateTopics
- s.PrivateRegexps = make([]*regexp.Regexp, len(privateTopics))
- for i, ts := range privateTopics {
+ s.AllowedPrivateTopics = allowedPrivateTopics
+ s.AllowedPrivateRegexps = make([]*regexp.Regexp, len(allowedPrivateTopics))
+ for i, ts := range allowedPrivateTopics {
var r *regexp.Regexp
if tpl, err := uritemplate.New(ts); err == nil {
r = tpl.Regexp()
}
- s.PrivateRegexps[i] = r
+ s.AllowedPrivateRegexps[i] = r
}
- s.EscapedTopics = escapeTopics(topics)
+ s.EscapedTopics = escapeTopics(subscribedTopics)
}
func escapeTopics(topics []string) []string {
@@ -160,36 +172,48 @@ func escapeTopics(topics []string) []string {
}
// MatchTopic checks if the current subscriber can access to the given topic.
-func (s *Subscriber) MatchTopic(topic string, private bool) (match bool) {
- for i, ts := range s.Topics {
- if ts == "*" || ts == topic {
- match = true
+//
+//nolint:gocognit
+func (s *Subscriber) MatchTopics(topics []string, private bool) bool {
+ var subscribed bool
+ canAccess := !private
- break
- }
+ for _, topic := range topics {
+ if !subscribed {
+ for i, ts := range s.SubscribedTopics {
+ if ts == "*" || ts == topic {
+ subscribed = true
+
+ break
+ }
- r := s.TopicRegexps[i]
- if r != nil && r.MatchString(topic) {
- match = true
+ r := s.SubscribedTopicRegexps[i]
+ if r != nil && r.MatchString(topic) {
+ subscribed = true
- break
+ break
+ }
+ }
}
- }
- if !match {
- return false
- }
- if !private {
- return true
- }
+ if !canAccess {
+ for i, ts := range s.AllowedPrivateTopics {
+ if ts == "*" || ts == topic {
+ canAccess = true
- for i, ts := range s.PrivateTopics {
- if ts == "*" || ts == topic {
- return true
+ break
+ }
+
+ r := s.AllowedPrivateRegexps[i]
+ if r != nil && r.MatchString(topic) {
+ canAccess = true
+
+ break
+ }
+ }
}
- r := s.PrivateRegexps[i]
- if r != nil && r.MatchString(topic) {
+ if subscribed && canAccess {
return true
}
}
@@ -199,20 +223,14 @@ func (s *Subscriber) MatchTopic(topic string, private bool) (match bool) {
// Match checks if the current subscriber can receive the given update.
func (s *Subscriber) Match(u *Update) bool {
- for _, t := range u.Topics {
- if s.MatchTopic(t, u.Private) {
- return true
- }
- }
-
- return false
+ return s.MatchTopics(u.Topics, u.Private)
}
// getSubscriptions return the list of subscriptions associated to this subscriber.
func (s *Subscriber) getSubscriptions(topic, context string, active bool) []subscription {
var subscriptions []subscription //nolint:prealloc
- for k, t := range s.Topics {
- if topic != "" && !s.MatchTopic(topic, false) {
+ for k, t := range s.SubscribedTopics {
+ if topic != "" && (!s.MatchTopics([]string{topic}, false) || t != topic) {
continue
}
@@ -240,16 +258,26 @@ func (s *Subscriber) MarshalLogObject(enc zapcore.ObjectEncoder) error {
if s.RemoteAddr != "" {
enc.AddString("remote_addr", s.RemoteAddr)
}
- if s.PrivateTopics != nil {
- if err := enc.AddArray("topic_selectors", stringArray(s.PrivateTopics)); err != nil {
+ if s.AllowedPrivateTopics != nil {
+ if err := enc.AddArray("topic_selectors", stringArray(s.AllowedPrivateTopics)); err != nil {
return fmt.Errorf("log error: %w", err)
}
}
- if s.Topics != nil {
- if err := enc.AddArray("topics", stringArray(s.Topics)); err != nil {
+ if s.SubscribedTopics != nil {
+ if err := enc.AddArray("topics", stringArray(s.SubscribedTopics)); err != nil {
return fmt.Errorf("log error: %w", err)
}
}
return nil
}
+
+// handleFullChan disconnects the subscriber when the out channel is full.
+func (s *Subscriber) handleFullChan() {
+ atomic.StoreInt32(&s.disconnected, 1)
+ s.outMutex.Unlock()
+
+ if c := s.logger.Check(zap.ErrorLevel, "subscriber unable to receive updates fast enough"); c != nil {
+ c.Write(zap.Object("subscriber", s))
+ }
+}
diff --git a/subscriber_list.go b/subscriber_list.go
index a24884d8..97221ab1 100644
--- a/subscriber_list.go
+++ b/subscriber_list.go
@@ -1,6 +1,7 @@
package mercure
import (
+ "sort"
"strings"
"github.com/kevburnsjr/skipfilter"
@@ -10,50 +11,106 @@ type SubscriberList struct {
skipfilter *skipfilter.SkipFilter
}
+// We choose a delimiter and an escape character which are unlikely to be used.
+const (
+ escape = '\x00'
+ delim = '\x01'
+)
+
+//nolint:gochecknoglobals
+var replacer = strings.NewReplacer(
+ string(escape), string([]rune{escape, escape}),
+ string(delim), string([]rune{escape, delim}),
+)
+
func NewSubscriberList(size int) *SubscriberList {
return &SubscriberList{
- skipfilter: skipfilter.New(func(s interface{}, topic interface{}) bool {
- p := strings.SplitN(topic.(string), "_", 2)
- if len(p) < 2 {
- return false
- }
-
- return s.(*Subscriber).MatchTopic(p[1], p[0] == "p")
+ skipfilter: skipfilter.New(func(s interface{}, filter interface{}) bool {
+ return s.(*Subscriber).MatchTopics(decode(filter.(string)))
}, size),
}
}
-func (sc *SubscriberList) MatchAny(u *Update) (res []*Subscriber) {
- scopedTopics := make([]interface{}, len(u.Topics))
- for i, t := range u.Topics {
- if u.Private {
- scopedTopics[i] = "p_" + t
- } else {
- scopedTopics[i] = "_" + t
+func encode(topics []string, private bool) string {
+ sort.Strings(topics)
+
+ parts := make([]string, len(topics)+1)
+ if private {
+ parts[0] = "1"
+ } else {
+ parts[0] = "0"
+ }
+
+ for i, t := range topics {
+ parts[i+1] = replacer.Replace(t)
+ }
+
+ return strings.Join(parts, string(delim))
+}
+
+func decode(f string) (topics []string, private bool) {
+ var (
+ privateExtracted, inEscape bool
+ builder strings.Builder
+ )
+
+ for _, char := range f {
+ if inEscape {
+ builder.WriteRune(char)
+ inEscape = false
+
+ continue
+ }
+
+ switch char {
+ case escape:
+ inEscape = true
+
+ case delim:
+ if !privateExtracted {
+ private = builder.String() == "1"
+ builder.Reset()
+
+ privateExtracted = true
+
+ break
+ }
+
+ topics = append(topics, builder.String())
+ builder.Reset()
+
+ default:
+ builder.WriteRune(char)
}
}
- for _, m := range sc.skipfilter.MatchAny(scopedTopics...) {
+ topics = append(topics, builder.String())
+
+ return topics, private
+}
+
+func (sl *SubscriberList) MatchAny(u *Update) (res []*Subscriber) {
+ for _, m := range sl.skipfilter.MatchAny(encode(u.Topics, u.Private)) {
res = append(res, m.(*Subscriber))
}
return
}
-func (sc *SubscriberList) Walk(start uint64, callback func(s *Subscriber) bool) uint64 {
- return sc.skipfilter.Walk(start, func(val interface{}) bool {
+func (sl *SubscriberList) Walk(start uint64, callback func(s *Subscriber) bool) uint64 {
+ return sl.skipfilter.Walk(start, func(val interface{}) bool {
return callback(val.(*Subscriber))
})
}
-func (sc *SubscriberList) Add(s *Subscriber) {
- sc.skipfilter.Add(s)
+func (sl *SubscriberList) Add(s *Subscriber) {
+ sl.skipfilter.Add(s)
}
-func (sc *SubscriberList) Remove(s *Subscriber) {
- sc.skipfilter.Remove(s)
+func (sl *SubscriberList) Remove(s *Subscriber) {
+ sl.skipfilter.Remove(s)
}
-func (sc *SubscriberList) Len() int {
- return sc.skipfilter.Len()
+func (sl *SubscriberList) Len() int {
+ return sl.skipfilter.Len()
}
diff --git a/subscriber_list_test.go b/subscriber_list_test.go
new file mode 100644
index 00000000..a52f6c76
--- /dev/null
+++ b/subscriber_list_test.go
@@ -0,0 +1,42 @@
+package mercure
+
+import (
+ "fmt"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "go.uber.org/zap"
+)
+
+func TestEncode(t *testing.T) {
+ e := encode([]string{"Foo\x00\x01Bar\x00Baz\x01", "\x01bar"}, true)
+ assert.Equal(t, "1\x01\x00\x01bar\x01Foo\x00\x00\x00\x01Bar\x00\x00Baz\x00\x01", e)
+}
+
+func TestDecode(t *testing.T) {
+ topics, private := decode("1\x01\x00\x01bar\x01Foo\x00\x00\x00\x01Bar\x00\x00Baz\x00\x01")
+
+ assert.Equal(t, []string{"\x01bar", "Foo\x00\x01Bar\x00Baz\x01"}, topics)
+ assert.True(t, private)
+}
+
+func BenchmarkSubscriberList(b *testing.B) {
+ logger := zap.NewNop()
+
+ l := NewSubscriberList(100)
+ for i := 0; i < 100; i++ {
+ s := NewSubscriber("", logger)
+ t := fmt.Sprintf("https://example.com/%d", (i % 10))
+ s.SetTopics([]string{"https://example.org/foo", t}, []string{"https://example.net/bar", t})
+
+ l.Add(s)
+ }
+
+ b.ResetTimer()
+
+ for i := 0; i < b.N; i++ {
+ assert.NotEmpty(b, l.MatchAny(&Update{Topics: []string{"https://example.org/foo"}}))
+ assert.Empty(b, l.MatchAny(&Update{Topics: []string{"https://example.org/baz"}}))
+ assert.NotEmpty(b, l.MatchAny(&Update{Topics: []string{"https://example.com/8"}, Private: false}))
+ }
+}
diff --git a/subscriber_test.go b/subscriber_test.go
index 823bbc69..1432ada9 100644
--- a/subscriber_test.go
+++ b/subscriber_test.go
@@ -10,15 +10,16 @@ import (
func TestDispatch(t *testing.T) {
s := NewSubscriber("1", zap.NewNop())
- s.Topics = []string{"http://example.com"}
+ s.SubscribedTopics = []string{"http://example.com"}
+ s.SubscribedTopics = []string{"http://example.com"}
defer s.Disconnect()
// Dispatch must be non-blocking
// Messages coming from the history can be sent after live messages, but must be received first
- s.Dispatch(&Update{Topics: s.Topics, Event: Event{ID: "3"}}, false)
- s.Dispatch(&Update{Topics: s.Topics, Event: Event{ID: "1"}}, true)
- s.Dispatch(&Update{Topics: s.Topics, Event: Event{ID: "4"}}, false)
- s.Dispatch(&Update{Topics: s.Topics, Event: Event{ID: "2"}}, true)
+ s.Dispatch(&Update{Topics: s.SubscribedTopics, Event: Event{ID: "3"}}, false)
+ s.Dispatch(&Update{Topics: s.SubscribedTopics, Event: Event{ID: "1"}}, true)
+ s.Dispatch(&Update{Topics: s.SubscribedTopics, Event: Event{ID: "4"}}, false)
+ s.Dispatch(&Update{Topics: s.SubscribedTopics, Event: Event{ID: "2"}}, true)
s.HistoryDispatched("")
s.Ready()
@@ -56,3 +57,28 @@ func TestLogSubscriber(t *testing.T) {
assert.Contains(t, log, `"topic_selectors":["https://example.com/foo"]`)
assert.Contains(t, log, `"topics":["https://example.com/bar"]`)
}
+
+func TestMatchTopic(t *testing.T) {
+ s := NewSubscriber("", zap.NewNop())
+ s.SetTopics([]string{"https://example.com/no-match", "https://example.com/books/{id}"}, []string{"https://example.com/users/foo/{?topic}"})
+
+ assert.False(t, s.Match(&Update{Topics: []string{"https://example.com/not-subscribed"}}))
+ assert.False(t, s.Match(&Update{Topics: []string{"https://example.com/not-subscribed"}, Private: true}))
+ assert.False(t, s.Match(&Update{Topics: []string{"https://example.com/no-match"}, Private: true}))
+ assert.False(t, s.Match(&Update{Topics: []string{"https://example.com/books/1"}, Private: true}))
+ assert.False(t, s.Match(&Update{Topics: []string{"https://example.com/books/1", "https://example.com/users/bar/?topic=https%3A%2F%2Fexample.com%2Fbooks%2F1"}, Private: true}))
+
+ assert.True(t, s.Match(&Update{Topics: []string{"https://example.com/books/1"}}))
+ assert.True(t, s.Match(&Update{Topics: []string{"https://example.com/books/1", "https://example.com/users/foo/?topic=https%3A%2F%2Fexample.com%2Fbooks%2F1"}, Private: true}))
+}
+
+func TestSubscriberDoesNotBlockWhenChanIsFull(t *testing.T) {
+ s := NewSubscriber("", zap.NewNop())
+ s.Ready()
+
+ for i := 0; i <= outBufferLength; i++ {
+ s.Dispatch(&Update{}, false)
+ }
+
+ assert.Equal(t, int32(1), s.disconnected)
+}
diff --git a/subscription.go b/subscription.go
index f227d338..4bfda051 100644
--- a/subscription.go
+++ b/subscription.go
@@ -31,9 +31,10 @@ type subscriptionCollection struct {
}
const (
- subscriptionURL = defaultHubURL + "/subscriptions/{topic}/{subscriber}"
- subscriptionsForTopicURL = defaultHubURL + "/subscriptions/{topic}"
- subscriptionsURL = defaultHubURL + "/subscriptions"
+ subscriptionsPath = "/subscriptions"
+ subscriptionURL = defaultHubURL + subscriptionsPath + "/{topic}/{subscriber}"
+ subscriptionsForTopicURL = defaultHubURL + subscriptionsPath + "/{topic}"
+ subscriptionsURL = defaultHubURL + subscriptionsPath
)
func (h *Hub) SubscriptionsHandler(w http.ResponseWriter, r *http.Request) {
@@ -103,13 +104,10 @@ func (h *Hub) SubscriptionHandler(w http.ResponseWriter, r *http.Request) {
}
func (h *Hub) initSubscription(currentURL string, w http.ResponseWriter, r *http.Request) (lastEventID string, subscribers []*Subscriber, ok bool) {
- if h.subscriberJWT != nil {
- claims, err := authorize(r, h.subscriberJWT, nil, h.cookieName)
+ if h.subscriberJWTKeyFunc != nil {
+ claims, err := authorize(r, h.subscriberJWTKeyFunc, nil, h.cookieName)
if err != nil || claims == nil || claims.Mercure.Subscribe == nil || !canReceive(h.topicSelectorStore, []string{currentURL}, claims.Mercure.Subscribe) {
- http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
- if c := h.logger.Check(zap.InfoLevel, "Topic selectors not matched, not provided or authorization error"); c != nil {
- c.Write(zap.String("remote_addr", r.RemoteAddr), zap.Error(err))
- }
+ h.httpAuthorizationError(w, r, err)
return "", nil, false
}
@@ -128,7 +126,7 @@ func (h *Hub) initSubscription(currentURL string, w http.ResponseWriter, r *http
}
w.WriteHeader(http.StatusInternalServerError)
- return
+ return lastEventID, subscribers, ok
}
if r.Header.Get("If-None-Match") == lastEventID {
w.WriteHeader(http.StatusNotModified)
diff --git a/subscription_test.go b/subscription_test.go
index 5911bae5..b832265b 100644
--- a/subscription_test.go
+++ b/subscription_test.go
@@ -16,23 +16,23 @@ import (
func TestSubscriptionsHandlerAccessDenied(t *testing.T) {
hub := createDummy()
- req := httptest.NewRequest("GET", subscriptionsURL, nil)
+ req := httptest.NewRequest(http.MethodGet, subscriptionsURL, nil)
w := httptest.NewRecorder()
hub.SubscriptionsHandler(w, req)
res := w.Result()
assert.Equal(t, http.StatusUnauthorized, res.StatusCode)
res.Body.Close()
- req = httptest.NewRequest("GET", subscriptionsURL, nil)
- req.AddCookie(&http.Cookie{Name: "mercureAuthorization", Value: createDummyAuthorizedJWT(hub, roleSubscriber, []string{"/.well-known/mercure/subscriptions/foo{/subscriber}"})})
+ req = httptest.NewRequest(http.MethodGet, subscriptionsURL, nil)
+ req.AddCookie(&http.Cookie{Name: "mercureAuthorization", Value: createDummyAuthorizedJWT(roleSubscriber, []string{"/.well-known/mercure/subscriptions/foo{/subscriber}"})})
w = httptest.NewRecorder()
hub.SubscriptionsHandler(w, req)
res = w.Result()
assert.Equal(t, http.StatusUnauthorized, res.StatusCode)
res.Body.Close()
- req = httptest.NewRequest("GET", defaultHubURL+"/subscriptions/bar", nil)
- req.AddCookie(&http.Cookie{Name: "mercureAuthorization", Value: createDummyAuthorizedJWT(hub, roleSubscriber, []string{"/.well-known/mercure/subscriptions/foo{/subscriber}"})})
+ req = httptest.NewRequest(http.MethodGet, defaultHubURL+subscriptionsPath+"/bar", nil)
+ req.AddCookie(&http.Cookie{Name: "mercureAuthorization", Value: createDummyAuthorizedJWT(roleSubscriber, []string{"/.well-known/mercure/subscriptions/foo{/subscriber}"})})
w = httptest.NewRecorder()
hub.SubscriptionsHandler(w, req)
res = w.Result()
@@ -43,15 +43,15 @@ func TestSubscriptionsHandlerAccessDenied(t *testing.T) {
func TestSubscriptionHandlerAccessDenied(t *testing.T) {
hub := createDummy()
- req := httptest.NewRequest("GET", defaultHubURL+"/subscriptions/bar/baz", nil)
+ req := httptest.NewRequest(http.MethodGet, defaultHubURL+subscriptionsPath+"/bar/baz", nil)
w := httptest.NewRecorder()
hub.SubscriptionHandler(w, req)
res := w.Result()
assert.Equal(t, http.StatusUnauthorized, res.StatusCode)
res.Body.Close()
- req = httptest.NewRequest("GET", defaultHubURL+"/subscriptions/bar/baz", nil)
- req.AddCookie(&http.Cookie{Name: "mercureAuthorization", Value: createDummyAuthorizedJWT(hub, roleSubscriber, []string{"/.well-known/mercure/subscriptions/foo{/subscriber}"})})
+ req = httptest.NewRequest(http.MethodGet, defaultHubURL+subscriptionsPath+"/bar/baz", nil)
+ req.AddCookie(&http.Cookie{Name: "mercureAuthorization", Value: createDummyAuthorizedJWT(roleSubscriber, []string{"/.well-known/mercure/subscriptions/foo{/subscriber}"})})
w = httptest.NewRecorder()
hub.SubscriptionHandler(w, req)
res = w.Result()
@@ -62,18 +62,18 @@ func TestSubscriptionHandlerAccessDenied(t *testing.T) {
func TestSubscriptionHandlersETag(t *testing.T) {
hub := createDummy()
- req := httptest.NewRequest("GET", defaultHubURL+"/subscriptions", nil)
+ req := httptest.NewRequest(http.MethodGet, defaultHubURL+subscriptionsPath, nil)
req.Header.Add("If-None-Match", EarliestLastEventID)
- req.AddCookie(&http.Cookie{Name: "mercureAuthorization", Value: createDummyAuthorizedJWT(hub, roleSubscriber, []string{"/.well-known/mercure/subscriptions"})})
+ req.AddCookie(&http.Cookie{Name: "mercureAuthorization", Value: createDummyAuthorizedJWT(roleSubscriber, []string{"/.well-known/mercure/subscriptions"})})
w := httptest.NewRecorder()
hub.SubscriptionsHandler(w, req)
res := w.Result()
assert.Equal(t, http.StatusNotModified, res.StatusCode)
res.Body.Close()
- req = httptest.NewRequest("GET", defaultHubURL+"/subscriptions/foo/bar", nil)
+ req = httptest.NewRequest(http.MethodGet, defaultHubURL+subscriptionsPath+"/foo/bar", nil)
req.Header.Add("If-None-Match", EarliestLastEventID)
- req.AddCookie(&http.Cookie{Name: "mercureAuthorization", Value: createDummyAuthorizedJWT(hub, roleSubscriber, []string{"/.well-known/mercure/subscriptions/foo/bar"})})
+ req.AddCookie(&http.Cookie{Name: "mercureAuthorization", Value: createDummyAuthorizedJWT(roleSubscriber, []string{"/.well-known/mercure/subscriptions/foo/bar"})})
w = httptest.NewRecorder()
hub.SubscriptionHandler(w, req)
res = w.Result()
@@ -86,14 +86,14 @@ func TestSubscriptionsHandler(t *testing.T) {
s1 := NewSubscriber("", zap.NewNop())
s1.SetTopics([]string{"http://example.com/foo"}, nil)
- require.Nil(t, hub.transport.AddSubscriber(s1))
+ require.NoError(t, hub.transport.AddSubscriber(s1))
s2 := NewSubscriber("", zap.NewNop())
s2.SetTopics([]string{"http://example.com/bar"}, nil)
- require.Nil(t, hub.transport.AddSubscriber(s2))
+ require.NoError(t, hub.transport.AddSubscriber(s2))
- req := httptest.NewRequest("GET", defaultHubURL+"/subscriptions", nil)
- req.AddCookie(&http.Cookie{Name: "mercureAuthorization", Value: createDummyAuthorizedJWT(hub, roleSubscriber, []string{"/.well-known/mercure/subscriptions"})})
+ req := httptest.NewRequest(http.MethodGet, defaultHubURL+subscriptionsPath, nil)
+ req.AddCookie(&http.Cookie{Name: "mercureAuthorization", Value: createDummyAuthorizedJWT(roleSubscriber, []string{"/.well-known/mercure/subscriptions"})})
w := httptest.NewRecorder()
hub.SubscriptionsHandler(w, req)
res := w.Result()
@@ -125,11 +125,11 @@ func TestSubscriptionsHandlerForTopic(t *testing.T) {
s1 := NewSubscriber("", zap.NewNop())
s1.SetTopics([]string{"http://example.com/foo"}, nil)
- require.Nil(t, hub.transport.AddSubscriber(s1))
+ require.NoError(t, hub.transport.AddSubscriber(s1))
s2 := NewSubscriber("", zap.NewNop())
s2.SetTopics([]string{"http://example.com/bar"}, nil)
- require.Nil(t, hub.transport.AddSubscriber(s2))
+ require.NoError(t, hub.transport.AddSubscriber(s2))
escapedBarTopic := url.QueryEscape("http://example.com/bar")
@@ -138,8 +138,8 @@ func TestSubscriptionsHandlerForTopic(t *testing.T) {
router.SkipClean(true)
router.HandleFunc(subscriptionsForTopicURL, hub.SubscriptionsHandler)
- req := httptest.NewRequest("GET", defaultHubURL+"/subscriptions/"+s2.EscapedTopics[0], nil)
- req.AddCookie(&http.Cookie{Name: "mercureAuthorization", Value: createDummyAuthorizedJWT(hub, roleSubscriber, []string{"/.well-known/mercure/subscriptions/" + s2.EscapedTopics[0]})})
+ req := httptest.NewRequest(http.MethodGet, defaultHubURL+subscriptionsPath+"/"+s2.EscapedTopics[0], nil)
+ req.AddCookie(&http.Cookie{Name: "mercureAuthorization", Value: createDummyAuthorizedJWT(roleSubscriber, []string{"/.well-known/mercure/subscriptions/" + s2.EscapedTopics[0]})})
w := httptest.NewRecorder()
hub.SubscriptionsHandler(w, req)
res := w.Result()
@@ -150,7 +150,7 @@ func TestSubscriptionsHandlerForTopic(t *testing.T) {
json.Unmarshal(w.Body.Bytes(), &subscriptions)
assert.Equal(t, "https://mercure.rocks/", subscriptions.Context)
- assert.Equal(t, defaultHubURL+"/subscriptions/"+escapedBarTopic, subscriptions.ID)
+ assert.Equal(t, defaultHubURL+subscriptionsPath+"/"+escapedBarTopic, subscriptions.ID)
assert.Equal(t, "Subscriptions", subscriptions.Type)
lastEventID, subscribers, _ := hub.transport.(TransportSubscribers).GetSubscribers()
@@ -170,19 +170,19 @@ func TestSubscriptionHandler(t *testing.T) {
otherS := NewSubscriber("", zap.NewNop())
otherS.SetTopics([]string{"http://example.com/other"}, nil)
- require.Nil(t, hub.transport.AddSubscriber(otherS))
+ require.NoError(t, hub.transport.AddSubscriber(otherS))
s := NewSubscriber("", zap.NewNop())
s.SetTopics([]string{"http://example.com/other", "http://example.com/{foo}"}, nil)
- require.Nil(t, hub.transport.AddSubscriber(s))
+ require.NoError(t, hub.transport.AddSubscriber(s))
router := mux.NewRouter()
router.UseEncodedPath()
router.SkipClean(true)
router.HandleFunc(subscriptionURL, hub.SubscriptionHandler)
- req := httptest.NewRequest("GET", defaultHubURL+"/subscriptions/"+s.EscapedTopics[1]+"/"+s.EscapedID, nil)
- req.AddCookie(&http.Cookie{Name: "mercureAuthorization", Value: createDummyAuthorizedJWT(hub, roleSubscriber, []string{"/.well-known/mercure/subscriptions{/topic}{/subscriber}"})})
+ req := httptest.NewRequest(http.MethodGet, defaultHubURL+subscriptionsPath+"/"+s.EscapedTopics[1]+"/"+s.EscapedID, nil)
+ req.AddCookie(&http.Cookie{Name: "mercureAuthorization", Value: createDummyAuthorizedJWT(roleSubscriber, []string{"/.well-known/mercure/subscriptions{/topic}{/subscriber}"})})
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
res := w.Result()
@@ -191,12 +191,12 @@ func TestSubscriptionHandler(t *testing.T) {
var subscription subscription
json.Unmarshal(w.Body.Bytes(), &subscription)
- expectedSub := s.getSubscriptions(s.Topics[1], "https://mercure.rocks/", true)[1]
+ expectedSub := s.getSubscriptions(s.SubscribedTopics[1], "https://mercure.rocks/", true)[0]
expectedSub.LastEventID, _, _ = hub.transport.(TransportSubscribers).GetSubscribers()
assert.Equal(t, expectedSub, subscription)
- req = httptest.NewRequest("GET", defaultHubURL+"/subscriptions/notexist/"+s.EscapedID, nil)
- req.AddCookie(&http.Cookie{Name: "mercureAuthorization", Value: createDummyAuthorizedJWT(hub, roleSubscriber, []string{"/.well-known/mercure/subscriptions{/topic}{/subscriber}"})})
+ req = httptest.NewRequest(http.MethodGet, defaultHubURL+subscriptionsPath+"/notexist/"+s.EscapedID, nil)
+ req.AddCookie(&http.Cookie{Name: "mercureAuthorization", Value: createDummyAuthorizedJWT(roleSubscriber, []string{"/.well-known/mercure/subscriptions{/topic}{/subscriber}"})})
w = httptest.NewRecorder()
router.ServeHTTP(w, req)
res = w.Result()
diff --git a/tests/use-go-deadlock.sh b/tests/use-go-deadlock.sh
index 60c23a54..0e689058 100755
--- a/tests/use-go-deadlock.sh
+++ b/tests/use-go-deadlock.sh
@@ -3,16 +3,16 @@
SEP="\n\t"
args=( "-i" )
-if [ "$(uname)" = "Darwin" ]; then
+if [[ "$(uname)" = "Darwin" ]]; then
SEP=$'\\\n\\\t'
args=( "-i" "" )
fi
-GO111MODULE=off go get golang.org/x/tools/cmd/goimports
+go install golang.org/x/tools/cmd/goimports@latest
find . -name "*.go" -exec sed "${args[@]}" -e "s#\"sync\"#\"sync\"${SEP}deadlock \"github.com/sasha-s/go-deadlock\"#" {} \;
find . -name "*.go" -exec sed "${args[@]}" -e 's#sync.RWMutex#deadlock.RWMutex#' {} {} \;
find . -name "*.go" -exec sed "${args[@]}" -e 's#sync.Mutex#deadlock.Mutex#' {} {} \;
goimports -w .
go get github.com/sasha-s/go-deadlock/...@master
-cd caddy
+cd caddy || exit
go get github.com/sasha-s/go-deadlock/...@master
diff --git a/topic_selector.go b/topic_selector.go
index 98c549d9..09f74eb6 100644
--- a/topic_selector.go
+++ b/topic_selector.go
@@ -8,8 +8,8 @@ import (
)
type TopicSelectorStoreCache interface {
- Get(interface{}) (interface{}, bool)
- Set(interface{}, interface{}, int64) bool
+ Get(key interface{}) (interface{}, bool)
+ Set(key interface{}, value interface{}, n int64) bool
}
// TopicSelectorStore caches compiled templates to improve memory and CPU usage.
diff --git a/topic_selector_lru_test.go b/topic_selector_lru_test.go
index f2d8d52d..d0cb158a 100644
--- a/topic_selector_lru_test.go
+++ b/topic_selector_lru_test.go
@@ -9,7 +9,7 @@ import (
func TestMatchLRU(t *testing.T) {
tss, err := NewTopicSelectorStoreLRU(DefaultTopicSelectorStoreLRUMaxEntriesPerShard, DefaultTopicSelectorStoreLRUMaxEntriesPerShard)
- require.Nil(t, err)
+ require.NoError(t, err)
assert.False(t, tss.match("foo", "bar"))
diff --git a/transport.go b/transport.go
index 3e252bca..f5b6645b 100644
--- a/transport.go
+++ b/transport.go
@@ -11,7 +11,7 @@ import (
const EarliestLastEventID = "earliest"
// TransportFactory is the factory to initialize a new transport.
-type TransportFactory = func(u *url.URL, l Logger, tss *TopicSelectorStore) (Transport, error)
+type TransportFactory = func(u *url.URL, l Logger) (Transport, error)
var (
transportFactories = make(map[string]TransportFactory) //nolint:gochecknoglobals
@@ -24,7 +24,7 @@ func RegisterTransportFactory(scheme string, factory TransportFactory) {
transportFactoriesMu.Unlock()
}
-func NewTransport(u *url.URL, l Logger, tss *TopicSelectorStore) (Transport, error) { //nolint:ireturn
+func NewTransport(u *url.URL, l Logger) (Transport, error) { //nolint:ireturn
transportFactoriesMu.RLock()
f, ok := transportFactories[u.Scheme]
transportFactoriesMu.RUnlock()
@@ -33,7 +33,7 @@ func NewTransport(u *url.URL, l Logger, tss *TopicSelectorStore) (Transport, err
return nil, &TransportError{dsn: u.Redacted(), msg: "no such transport available"}
}
- return f(u, l, nil)
+ return f(u, l)
}
// Transport provides methods to dispatch and persist updates.
@@ -44,7 +44,7 @@ type Transport interface {
// AddSubscriber adds a new subscriber to the transport.
AddSubscriber(s *Subscriber) error
- // RemoveSubscriber removes a new subscriber from the transport.
+ // RemoveSubscriber removes a subscriber from the transport.
RemoveSubscriber(s *Subscriber) error
// Close closes the Transport.
@@ -86,3 +86,13 @@ func (e *TransportError) Error() string {
func (e *TransportError) Unwrap() error {
return e.err
}
+
+func getSubscribers(sl *SubscriberList) (subscribers []*Subscriber) {
+ sl.Walk(0, func(s *Subscriber) bool {
+ subscribers = append(subscribers, s)
+
+ return true
+ })
+
+ return
+}
diff --git a/update_test.go b/update_test.go
index c34722e8..65f74cc8 100644
--- a/update_test.go
+++ b/update_test.go
@@ -6,6 +6,7 @@ import (
"github.com/gofrs/uuid"
"github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
"go.uber.org/zap"
)
@@ -23,7 +24,7 @@ func TestAssignUUID(t *testing.T) {
assert.True(t, strings.HasPrefix(u.ID, "urn:uuid:"))
_, err := uuid.FromString(strings.TrimPrefix(u.ID, "urn:uuid:"))
- assert.Nil(t, err)
+ require.NoError(t, err)
}
func TestLogUpdate(t *testing.T) {