├── .gitignore ├── tests ├── key ├── secret └── keysecret.yaml ├── .github ├── CODEOWNERS └── workflows │ ├── test.yaml │ ├── lint.yaml │ ├── docker.yaml │ └── release.yaml ├── LICENSE-COMMERCIAL ├── renovate.json ├── Dockerfile ├── go.mod ├── README.md ├── main.go ├── go.sum ├── LICENSE └── main_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | lk-jwt-service 3 | -------------------------------------------------------------------------------- /tests/key: -------------------------------------------------------------------------------- 1 | from_file_oquusheiheiw4Iegah8te3Vienguus5a 2 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @element-hq/element-call-reviewers 2 | -------------------------------------------------------------------------------- /tests/secret: -------------------------------------------------------------------------------- 1 | from_file_vohmahH3eeyieghohSh3kee8feuPhaim 2 | -------------------------------------------------------------------------------- /tests/keysecret.yaml: -------------------------------------------------------------------------------- 1 | keysecret_iethuB2LeLiNuishiaKeephei9jaatio: keysecret_xefaingo4oos6ohla9phiMieBu3ohJi2 2 | -------------------------------------------------------------------------------- /LICENSE-COMMERCIAL: -------------------------------------------------------------------------------- 1 | Licensees holding a valid commercial license with Element may use this 2 | software in accordance with the terms contained in a written agreement 3 | between you and Element. 4 | 5 | To purchase a commercial license please contact our sales team at 6 | licensing@element.io 7 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: [main] 7 | 8 | jobs: 9 | test: 10 | name: Testing 11 | runs-on: ubuntu-latest 12 | permissions: 13 | contents: read 14 | steps: 15 | - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 16 | - name: Install Go 17 | uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 18 | with: 19 | go-version-file: go.mod 20 | - name: Test 21 | run: go test -timeout 30s 22 | -------------------------------------------------------------------------------- /.github/workflows/lint.yaml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: 4 | pull_request: {} 5 | push: 6 | branches: [main] 7 | jobs: 8 | lint: 9 | timeout-minutes: 5 10 | name: Linting 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 14 | - name: Install Go 15 | uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 16 | with: 17 | go-version-file: go.mod 18 | - name: golangci-lint 19 | uses: golangci/golangci-lint-action@4afd733a84b1f43292c63897423277bb7f4313a9 # v8.0.0 20 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:recommended", 5 | "schedule:monthly", 6 | "helpers:pinGitHubActionDigestsToSemver", 7 | ":enableVulnerabilityAlertsWithLabel(security)" 8 | ], 9 | "addLabels": ["dependencies"], 10 | "vulnerabilityAlerts": { 11 | "schedule": [ 12 | "at any time" 13 | ], 14 | "prHourlyLimit": 0, 15 | "minimumReleaseAge": null 16 | }, 17 | "packageRules": [ 18 | { 19 | "groupName": "GitHub Actions", 20 | "matchDepTypes": ["action"], 21 | "pinDigests": true 22 | } 23 | ], 24 | "minimumReleaseAge": "5 days" 25 | } 26 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Set the version to match that which is in go.mod 2 | ARG GO_VERSION="build-arg-must-be-provided" 3 | 4 | FROM --platform=${BUILDPLATFORM} golang:${GO_VERSION}-alpine AS builder 5 | 6 | WORKDIR /proj 7 | 8 | COPY go.mod ./ 9 | COPY go.sum ./ 10 | RUN go mod download 11 | 12 | COPY *.go ./ 13 | 14 | ARG TARGETOS TARGETARCH 15 | RUN GOOS=$TARGETOS GOARCH=$TARGETARCH go build -o lk-jwt-service 16 | # set up nsswitch.conf for Go's "netgo" implementation 17 | # - https://github.com/golang/go/blob/go1.24.0/src/net/conf.go#L343 18 | RUN echo 'hosts: files dns' > /etc/nsswitch.conf 19 | 20 | FROM scratch 21 | 22 | COPY --from=builder /proj/lk-jwt-service /lk-jwt-service 23 | COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ 24 | COPY --from=builder /etc/nsswitch.conf /etc/nsswitch.conf 25 | 26 | EXPOSE 8080 27 | 28 | CMD [ "/lk-jwt-service" ] 29 | -------------------------------------------------------------------------------- /.github/workflows/docker.yaml: -------------------------------------------------------------------------------- 1 | name: Build and publish Docker image 2 | 3 | on: 4 | push: 5 | 6 | env: 7 | REGISTRY: ghcr.io 8 | IMAGE_NAME: ${{ github.repository }} 9 | 10 | jobs: 11 | build-and-push-image: 12 | runs-on: ubuntu-latest 13 | permissions: 14 | contents: read 15 | packages: write 16 | 17 | steps: 18 | - name: Get current time 19 | id: current-time 20 | run: echo "unix_time=$(date +'%s')" >> $GITHUB_OUTPUT 21 | 22 | - name: Checkout repository 23 | uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 24 | 25 | - name: Log in to the Container registry 26 | uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0 27 | with: 28 | registry: ${{ env.REGISTRY }} 29 | username: ${{ github.actor }} 30 | password: ${{ secrets.GITHUB_TOKEN }} 31 | 32 | - name: Extract metadata (tags, labels) for Docker 33 | id: meta 34 | uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f # v5.8.0 35 | with: 36 | images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} 37 | tags: | 38 | type=sha,format=short,event=branch 39 | type=ref,event=pr 40 | type=semver,pattern={{version}} 41 | type=raw,value=latest-ci_${{steps.current-time.outputs.unix_time}},enable={{is_default_branch}} 42 | latest-ci 43 | 44 | - name: Set up Docker Buildx 45 | uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1 46 | 47 | - name: Get go version 48 | run: echo "GO_VERSION=$(go mod edit -json | jq -r .Toolchain | sed "s,^go,,")" >> $GITHUB_ENV 49 | 50 | - name: Build and push Docker image 51 | uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0 52 | with: 53 | context: . 54 | platforms: linux/amd64,linux/arm64 55 | push: ${{ github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v') }} # only push on main branch or release tag 56 | provenance: mode=max 57 | sbom: true 58 | tags: ${{ steps.meta.outputs.tags }} 59 | labels: ${{ steps.meta.outputs.labels }} 60 | build-args: | 61 | GO_VERSION=${{ env.GO_VERSION }} 62 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: "Create draft release after tag" 2 | on: 3 | push: 4 | tags: ["v*"] 5 | permissions: 6 | contents: write 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | strategy: 12 | matrix: 13 | os: ["linux"] 14 | arch: ["amd64", "arm64"] 15 | steps: 16 | - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 17 | - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 18 | with: 19 | go-version-file: go.mod 20 | - run: mkdir build 21 | - run: go build -trimpath -o build/lk-jwt-service_${{ matrix.os }}_${{ matrix.arch }} 22 | env: 23 | CGO_ENABLED: 0 24 | GOOS: ${{ matrix.os }} 25 | GOARCH: ${{ matrix.arch }} 26 | - name: "Upload binary as artifact" 27 | uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 28 | with: 29 | name: build-${{ matrix.os }}-${{ matrix.arch }} 30 | path: build/lk-jwt-service_${{ matrix.os }}_${{ matrix.arch }} 31 | if-no-files-found: error 32 | 33 | create-release: 34 | needs: ["build"] 35 | runs-on: ubuntu-latest 36 | steps: 37 | - name: "Extract version" 38 | run: echo "IMAGE_VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV 39 | - name: "Fetch all binaries" 40 | uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 41 | with: 42 | path: build 43 | pattern: build-* 44 | merge-multiple: true 45 | - name: "Create release" 46 | uses: softprops/action-gh-release@6da8fa9354ddfdc4aeace5fc48d7f679b5214090 # v2.4.1 47 | with: 48 | files: build/* 49 | fail_on_unmatched_files: true 50 | draft: true 51 | generate_release_notes: true 52 | body: | 53 | ## Docker image 54 | 55 | The service is available as a Docker image from the [GitHub Container Registry](https://github.com/element-hq/lk-jwt-service/pkgs/container/lk-jwt-service). 56 | 57 | ``` 58 | docker pull ghcr.io/element-hq/lk-jwt-service:${{env.IMAGE_VERSION}} 59 | ``` 60 | 61 | ## Precompiled binaries 62 | 63 | The service is available as static precompiled binaries for amd64 and arm64 on linux attached to this release below. 64 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module lk-jwt-service 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.25.3 6 | 7 | require ( 8 | github.com/golang-jwt/jwt/v5 v5.3.0 9 | github.com/livekit/protocol v1.34.0 10 | github.com/livekit/server-sdk-go/v2 v2.5.0 11 | github.com/matrix-org/gomatrix v0.0.0-20220926102614-ceba4d9f7530 12 | github.com/matrix-org/gomatrixserverlib v0.0.0-20250815065806-6697d93cbcba 13 | ) 14 | 15 | require ( 16 | buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.0-20241127180247-a33202765966.1 // indirect 17 | buf.build/go/protoyaml v0.3.1 // indirect 18 | cel.dev/expr v0.19.0 // indirect 19 | github.com/antlr4-go/antlr/v4 v4.13.0 // indirect 20 | github.com/benbjohnson/clock v1.3.5 // indirect 21 | github.com/bep/debounce v1.2.1 // indirect 22 | github.com/bufbuild/protovalidate-go v0.8.0 // indirect 23 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 24 | github.com/dennwc/iters v1.0.1 // indirect 25 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect 26 | github.com/frostbyte73/core v0.1.1 // indirect 27 | github.com/fsnotify/fsnotify v1.8.0 // indirect 28 | github.com/gammazero/deque v1.0.0 // indirect 29 | github.com/go-jose/go-jose/v3 v3.0.4 // indirect 30 | github.com/go-logr/logr v1.4.2 // indirect 31 | github.com/go-logr/stdr v1.2.2 // indirect 32 | github.com/google/cel-go v0.22.1 // indirect 33 | github.com/google/uuid v1.6.0 // indirect 34 | github.com/gorilla/websocket v1.5.3 // indirect 35 | github.com/hashicorp/go-set/v3 v3.0.0 // indirect 36 | github.com/jxskiss/base62 v1.1.0 // indirect 37 | github.com/klauspost/compress v1.17.11 // indirect 38 | github.com/klauspost/cpuid/v2 v2.2.7 // indirect 39 | github.com/kr/pretty v0.3.1 // indirect 40 | github.com/lithammer/shortuuid/v4 v4.2.0 // indirect 41 | github.com/livekit/mageutil v0.0.0-20230125210925-54e8a70427c1 // indirect 42 | github.com/livekit/mediatransportutil v0.0.0-20241220010243-a2bdee945564 // indirect 43 | github.com/livekit/psrpc v0.6.1-0.20250205181828-a0beed2e4126 // indirect 44 | github.com/magefile/mage v1.15.0 // indirect 45 | github.com/matrix-org/util v0.0.0-20221111132719-399730281e66 // indirect 46 | github.com/nats-io/nats.go v1.38.0 // indirect 47 | github.com/nats-io/nkeys v0.4.9 // indirect 48 | github.com/nats-io/nuid v1.0.1 // indirect 49 | github.com/oleiade/lane/v2 v2.0.0 // indirect 50 | github.com/pion/datachannel v1.5.10 // indirect 51 | github.com/pion/dtls/v3 v3.0.4 // indirect 52 | github.com/pion/ice/v4 v4.0.6 // indirect 53 | github.com/pion/interceptor v0.1.39 // indirect 54 | github.com/pion/logging v0.2.3 // indirect 55 | github.com/pion/mdns/v2 v2.0.7 // indirect 56 | github.com/pion/randutil v0.1.0 // indirect 57 | github.com/pion/rtcp v1.2.15 // indirect 58 | github.com/pion/rtp v1.8.18 // indirect 59 | github.com/pion/sctp v1.8.35 // indirect 60 | github.com/pion/sdp/v3 v3.0.10 // indirect 61 | github.com/pion/srtp/v3 v3.0.4 // indirect 62 | github.com/pion/stun/v3 v3.0.0 // indirect 63 | github.com/pion/transport/v3 v3.0.7 // indirect 64 | github.com/pion/turn/v4 v4.0.0 // indirect 65 | github.com/pion/webrtc/v4 v4.0.9 // indirect 66 | github.com/puzpuzpuz/xsync/v3 v3.5.0 // indirect 67 | github.com/redis/go-redis/v9 v9.7.3 // indirect 68 | github.com/sirupsen/logrus v1.9.3 // indirect 69 | github.com/stoewer/go-strcase v1.3.0 // indirect 70 | github.com/tidwall/gjson v1.18.0 // indirect 71 | github.com/tidwall/match v1.1.1 // indirect 72 | github.com/tidwall/pretty v1.2.1 // indirect 73 | github.com/tidwall/sjson v1.2.5 // indirect 74 | github.com/twitchtv/twirp v8.1.3+incompatible // indirect 75 | github.com/wlynxg/anet v0.0.5 // indirect 76 | github.com/zeebo/xxh3 v1.0.2 // indirect 77 | go.uber.org/atomic v1.11.0 // indirect 78 | go.uber.org/multierr v1.11.0 // indirect 79 | go.uber.org/zap v1.27.0 // indirect 80 | go.uber.org/zap/exp v0.3.0 // indirect 81 | golang.org/x/crypto v0.38.0 // indirect 82 | golang.org/x/exp v0.0.0-20250207012021-f9890c6ad9f3 // indirect 83 | golang.org/x/net v0.40.0 // indirect 84 | golang.org/x/sync v0.14.0 // indirect 85 | golang.org/x/sys v0.33.0 // indirect 86 | golang.org/x/text v0.25.0 // indirect 87 | google.golang.org/genproto/googleapis/api v0.0.0-20241202173237-19429a94021a // indirect 88 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250204164813-702378808489 // indirect 89 | google.golang.org/grpc v1.70.0 // indirect 90 | google.golang.org/protobuf v1.36.5 // indirect 91 | gopkg.in/yaml.v3 v3.0.1 // indirect 92 | ) 93 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🎥 MatrixRTC Authorization Service 2 | 3 | The **MatrixRTC Authorization Service** bridges Matrix and LiveKit, handling 4 | authentication and room creation when needed. 5 | 6 | ## 💡 TL;DR 7 | 8 | Matrix user wants to start or join a call? 9 | 10 | 👤 ➡️ Gets OpenID token ➡️ Sends it to the **MatrixRTC Authorization Service** ➡️ 11 | Receives LiveKit JWT ➡️ 12 | 13 | - **If full-access user** ➡️ Can trigger LiveKit room creation (if needed) ➡️ 14 | Joins the call 🎉 15 | - **If restricted user** ➡️ Can join existing rooms ➡️ Joins the call 🎉 16 | 17 | 📡 Once connected, the LiveKit SFU handles all real-time media routing so 18 | participants can see and hear each other. 19 | 20 | ## 🏗️ MatrixRTC Stack: Architecture Overview 21 | 22 |
23 |
24 |
LIVEKIT_{KEY|SECRET} | |
125 | | `LIVEKIT_JWT_BIND` | Address to bind the server to | ❌ No, ⚠️ mutually exclusive with `LIVEKIT_JWT_PORT` | `:8080` |
126 | | `LIVEKIT_JWT_PORT` | ⚠️ Deprecated Port to bind the server to | ❌ No, ⚠️ mutually exclusive with `LIVEKIT_JWT_BIND` | |
127 | | `LIVEKIT_FULL_ACCESS_HOMESERVERS` | Comma-separated list of full-access homeservers (`*` for all) | ❌ No | `*` |
128 |
129 | > [!IMPORTANT]
130 | > By default, the LiveKit SFU auto-creates rooms for all users. To ensure proper
131 | > access control, update your LiveKit
132 | > [config.yaml](https://github.com/livekit/livekit/blob/7350e9933107ecdea4ada8f8bcb0d6ca78b3f8f7/config-sample.yaml#L170)
133 | > to **disable automatic room creation**.
134 |
135 | **LiveKit SFU config should include:**
136 |
137 | ```yaml
138 | room:
139 | auto_create: false
140 | ```
141 |
142 | ## 🔒 Transport Layer Security (TLS) Setup Using a Reverse Proxy
143 |
144 | To properly secure the MatrixRTC Authorization Service, a reverse proxy is
145 | recommended.
146 |
147 | ### Example Caddy Config
148 |
149 | ```caddy
150 | matrix-rtc.domain.tld {
151 | bind xx.xx.xx.xx
152 |
153 | handle /livekit/jwt* {
154 | reverse_proxy localhost:8080
155 | }
156 | }
157 | ```
158 |
159 | ### Example Nginx Config
160 |
161 | ```nginx
162 | server {
163 | listen 80;
164 | server_name matrix-rtc.domain.tld;
165 |
166 | # Redirect HTTP → HTTPS
167 | return 301 https://$host$request_uri;
168 | }
169 |
170 | server {
171 | listen 443 ssl;
172 | server_name matrix-rtc.domain.tld;
173 |
174 | # TLS certificate paths (replace with your own)
175 | ssl_certificate /etc/ssl/certs/matrix-rtc.crt;
176 | ssl_certificate_key /etc/ssl/private/matrix-rtc.key;
177 |
178 | # TLS settings (minimal)
179 | ssl_protocols TLSv1.2 TLSv1.3;
180 | ssl_ciphers HIGH:!aNULL:!MD5;
181 |
182 | location /livekit/jwt/ {
183 | proxy_pass http://localhost:8080/;
184 | proxy_set_header Host $host;
185 | proxy_set_header X-Real-IP $remote_addr;
186 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
187 | proxy_set_header X-Forwarded-Proto $scheme;
188 | }
189 | }
190 | ```
191 |
192 | ## 📌 Do Not Forget to Update Your Matrix Site's `.well-known/matrix/client`
193 |
194 | For proper MatrixRTC functionality, you need to configure your site's
195 | `.well-known/matrix/client`. See the
196 | [Element Call self-hosting guide](https://github.com/element-hq/element-call/blob/livekit/docs/self-hosting.md#matrixrtc-backend-announcement)
197 | for reference.
198 |
199 | The following key must be included in
200 | `https://domain.tld/.well-known/matrix/client`:
201 |
202 | ```json
203 | "org.matrix.msc4143.rtc_foci": [
204 | {
205 | "type": "livekit",
206 | "livekit_service_url": "https://matrix-rtc.domain.tld/livekit/jwt"
207 | }
208 | ]
209 | ```
210 |
211 | ## 🧪 Development & Testing
212 |
213 | ### Disable TLS verification
214 |
215 | For testing and debugging (e.g. in the absence of trusted certificates while
216 | testing in a lab), you can disable TLS verification for the outgoing connection
217 | to the Matrix homeserver by setting the environment variable
218 | `LIVEKIT_INSECURE_SKIP_VERIFY_TLS` to `YES_I_KNOW_WHAT_I_AM_DOING`.
219 |
220 | ### 🛠️ Development Environment (Docker Compose)
221 |
222 | Based on the
223 | [Element Call GitHub repo](https://github.com/element-hq/element-call)
224 |
225 | The easiest way to spin up the full Matrix stack is by using the development
226 | environment provided by Element Call. For detailed instructions, see
227 | [Element Call Backend Setup](https://github.com/element-hq/element-call?tab=readme-ov-file#backend).
228 |
229 | > [!NOTE]
230 | > To ensure your local frontend works properly, you need to add certificate
231 | > exceptions in your browser for:
232 | >
233 | > - `https://localhost:3000`
234 | > - `https://matrix-rtc.m.localhost/livekit/jwt/healthz`
235 | > - `https://synapse.m.localhost/.well-known/matrix/client`
236 | >
237 | > You can do this either by adding the minimal m.localhost CA
238 | > ([dev_tls_m.localhost.crt](https://raw.githubusercontent.com/element-hq/element-call/refs/heads/livekit/backend/dev_tls_m.localhost.crt))
239 | > to your browser’s trusted certificates, or by visiting each URL in your
240 | > browser and following the prompts to accept the exception.
241 |
242 | #### 🐳 Start MatrixRTC stack without the MatrixRTC Authorization Service
243 |
244 | ```sh
245 | git clone https://github.com/element-hq/element-call.git
246 | cd element-call
247 | docker-compose -f ./dev-backend-docker-compose.yml -f ./playwright-backend-docker-compose.override.yml up nginx livekit synapse redis
248 | ```
249 |
250 | #### 🔑 Start the MatrixRTC Authorization Service locally
251 |
252 | ```sh
253 | git clone https://github.com/element-hq/lk-jwt-service
254 | cd lk-jwt-service
255 | LIVEKIT_INSECURE_SKIP_VERIFY_TLS="YES_I_KNOW_WHAT_I_AM_DOING" \
256 | LIVEKIT_URL="wss://matrix-rtc.m.localhost/livekit/sfu" \
257 | LIVEKIT_KEY=devkey \
258 | LIVEKIT_SECRET=secret \
259 | LIVEKIT_JWT_PORT=6080 \
260 | LIVEKIT_FULL_ACCESS_HOMESERVERS=synapse.m.localhost \
261 | ./lk-jwt-service
262 | ```
263 |
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | // Copyright 2025 Element Creations Ltd.
2 | // Copyright 2023 - 2025 New Vector Ltd.
3 | //
4 | // SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
5 | // Please see LICENSE files in the repository root for full details.
6 |
7 | package main
8 |
9 | import (
10 | "context"
11 | "crypto/sha256"
12 | "crypto/tls"
13 | "encoding/json"
14 | "errors"
15 | "fmt"
16 | "io"
17 | "log"
18 | "net/http"
19 | "os"
20 | "slices"
21 | "strings"
22 |
23 | "time"
24 |
25 | "github.com/livekit/protocol/auth"
26 | "github.com/livekit/protocol/livekit"
27 | lksdk "github.com/livekit/server-sdk-go/v2"
28 |
29 | "github.com/matrix-org/gomatrix"
30 | "github.com/matrix-org/gomatrixserverlib/fclient"
31 | "github.com/matrix-org/gomatrixserverlib/spec"
32 | )
33 |
34 | type Handler struct {
35 | key, secret, lkUrl string
36 | fullAccessHomeservers []string
37 | skipVerifyTLS bool
38 | }
39 | type Config struct {
40 | Key string
41 | Secret string
42 | LkUrl string
43 | SkipVerifyTLS bool
44 | FullAccessHomeservers []string
45 | LkJwtBind string
46 | }
47 | type MatrixRTCMemberType struct {
48 | ID string `json:"id"`
49 | ClaimedUserID string `json:"claimed_user_id"`
50 | ClaimedDeviceID string `json:"claimed_device_id"`
51 | }
52 |
53 | type OpenIDTokenType struct {
54 | AccessToken string `json:"access_token"`
55 | TokenType string `json:"token_type"`
56 | MatrixServerName string `json:"matrix_server_name"`
57 | ExpiresIn int `json:"expires_in"`
58 | }
59 |
60 | type LegacySFURequest struct {
61 | Room string `json:"room"`
62 | OpenIDToken OpenIDTokenType `json:"openid_token"`
63 | DeviceID string `json:"device_id"`
64 | }
65 |
66 | type SFURequest struct {
67 | RoomID string `json:"room_id"`
68 | SlotID string `json:"slot_id"`
69 | OpenIDToken OpenIDTokenType `json:"openid_token"`
70 | Member MatrixRTCMemberType `json:"member"`
71 | DelayedEventID string `json:"delayed_event_id"`
72 | }
73 | type SFUResponse struct {
74 | URL string `json:"url"`
75 | JWT string `json:"jwt"`
76 | }
77 |
78 | type MatrixErrorResponse struct {
79 | Status int
80 | ErrCode string
81 | Err string
82 | }
83 |
84 | type ValidatableSFURequest interface {
85 | Validate() error
86 | }
87 |
88 | func (e *MatrixErrorResponse) Error() string {
89 | return e.Err
90 | }
91 |
92 | func (r *SFURequest) Validate() error {
93 | if r.RoomID == "" || r.SlotID == "" {
94 | log.Printf("Missing room_id or slot_id: room_id='%s', slot_id='%s'", r.RoomID, r.SlotID)
95 | return &MatrixErrorResponse{
96 | Status: http.StatusBadRequest,
97 | ErrCode: "M_BAD_JSON",
98 | Err: "The request body is missing `room_id` or `slot_id`",
99 | }
100 | }
101 | if r.Member.ID == "" || r.Member.ClaimedUserID == "" || r.Member.ClaimedDeviceID == "" {
102 | log.Printf("Missing member parameters: %+v", r.Member)
103 | return &MatrixErrorResponse{
104 | Status: http.StatusBadRequest,
105 | ErrCode: "M_BAD_JSON",
106 | Err: "The request body `member` is missing a `id`, `claimed_user_id` or `claimed_device_id`",
107 | }
108 | }
109 | if r.OpenIDToken.AccessToken == "" || r.OpenIDToken.MatrixServerName == "" {
110 | log.Printf("Missing OpenID token parameters: %+v", r.OpenIDToken)
111 | return &MatrixErrorResponse{
112 | Status: http.StatusBadRequest,
113 | ErrCode: "M_BAD_JSON",
114 | Err: "The request body `openid_token` is missing a `access_token` or `matrix_server_name`",
115 | }
116 | }
117 | return nil
118 | }
119 |
120 | func (r *LegacySFURequest) Validate() error {
121 | if r.Room == "" {
122 | return &MatrixErrorResponse{
123 | Status: http.StatusBadRequest,
124 | ErrCode: "M_BAD_JSON",
125 | Err: "Missing room parameter",
126 | }
127 | }
128 | if r.OpenIDToken.AccessToken == "" || r.OpenIDToken.MatrixServerName == "" {
129 | return &MatrixErrorResponse{
130 | Status: http.StatusBadRequest,
131 | ErrCode: "M_BAD_JSON",
132 | Err: "Missing OpenID token parameters",
133 | }
134 | }
135 | return nil
136 | }
137 |
138 | // writeMatrixError writes a Matrix-style error response to the HTTP response writer.
139 | func writeMatrixError(w http.ResponseWriter, status int, errCode string, errMsg string) {
140 | w.WriteHeader(status)
141 | if err := json.NewEncoder(w).Encode(gomatrix.RespError{
142 | ErrCode: errCode,
143 | Err: errMsg,
144 | }); err != nil {
145 | log.Printf("failed to encode json error message! %v", err)
146 | }
147 | }
148 |
149 | func getJoinToken(apiKey, apiSecret, room, identity string) (string, error) {
150 | at := auth.NewAccessToken(apiKey, apiSecret)
151 |
152 | canPublish := true
153 | canSubscribe := true
154 | grant := &auth.VideoGrant{
155 | RoomJoin: true,
156 | RoomCreate: false,
157 | CanPublish: &canPublish,
158 | CanSubscribe: &canSubscribe,
159 | Room: room,
160 | }
161 |
162 | at.SetVideoGrant(grant).
163 | SetIdentity(identity).
164 | SetValidFor(time.Hour)
165 |
166 | return at.ToJWT()
167 | }
168 |
169 | var exchangeOpenIdUserInfo = func(
170 | ctx context.Context, token OpenIDTokenType, skipVerifyTLS bool,
171 | ) (*fclient.UserInfo, error) {
172 | if token.AccessToken == "" || token.MatrixServerName == "" {
173 | return nil, errors.New("missing parameters in openid token")
174 | }
175 |
176 | if skipVerifyTLS {
177 | log.Printf("!!! WARNING !!! Skipping TLS verification for matrix client connection to %s", token.MatrixServerName)
178 | // Disable TLS verification on the default HTTP Transport for the well-known lookup
179 | http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
180 | }
181 | client := fclient.NewClient(fclient.WithWellKnownSRVLookups(true), fclient.WithSkipVerify(skipVerifyTLS))
182 |
183 | // validate the openid token by getting the user's ID
184 | userinfo, err := client.LookupUserInfo(
185 | ctx, spec.ServerName(token.MatrixServerName), token.AccessToken,
186 | )
187 | if err != nil {
188 | log.Printf("Failed to look up user info: %v", err)
189 | return nil, errors.New("failed to look up user info")
190 | }
191 | return &userinfo, nil
192 | }
193 |
194 | func (h *Handler) isFullAccessUser(matrixServerName string) bool {
195 | // Grant full access if wildcard '*' is present as the only entry
196 | if len(h.fullAccessHomeservers) == 1 && h.fullAccessHomeservers[0] == "*" {
197 | return true
198 | }
199 |
200 | // Check if the matrixServerName is in the list of full-access homeservers
201 | return slices.Contains(h.fullAccessHomeservers, matrixServerName)
202 | }
203 |
204 | func (h *Handler) processLegacySFURequest(r *http.Request, req *LegacySFURequest) (*SFUResponse, error) {
205 | // Note LegacySFURequest has already been validated at this point
206 |
207 | userInfo, err := exchangeOpenIdUserInfo(r.Context(), req.OpenIDToken, h.skipVerifyTLS)
208 | if err != nil {
209 | return nil, &MatrixErrorResponse{
210 | Status: http.StatusInternalServerError,
211 | ErrCode: "M_LOOKUP_FAILED",
212 | Err: "Failed to look up user info from homeserver",
213 | }
214 | }
215 |
216 | isFullAccessUser := h.isFullAccessUser(req.OpenIDToken.MatrixServerName)
217 |
218 | log.Printf(
219 | "Got Matrix user info for %s (%s)",
220 | userInfo.Sub,
221 | map[bool]string{true: "full access", false: "restricted access"}[isFullAccessUser],
222 | )
223 |
224 | // TODO: is DeviceID required? If so then we should have validated at the start
225 | lkIdentity := userInfo.Sub + ":" + req.DeviceID
226 | token, err := getJoinToken(h.key, h.secret, req.Room, lkIdentity)
227 | if err != nil {
228 | return nil, &MatrixErrorResponse{
229 | Status: http.StatusInternalServerError,
230 | ErrCode: "M_UNKNOWN",
231 | Err: "Internal Server Error",
232 | }
233 | }
234 |
235 | if isFullAccessUser {
236 | if err := createLiveKitRoom(r.Context(), h, req.Room, userInfo.Sub, lkIdentity); err != nil {
237 | return nil, &MatrixErrorResponse{
238 | Status: http.StatusInternalServerError,
239 | ErrCode: "M_UNKNOWN",
240 | Err: "Unable to create room on SFU",
241 | }
242 | }
243 | }
244 |
245 | return &SFUResponse{URL: h.lkUrl, JWT: token}, nil
246 | }
247 |
248 | func (h *Handler) processSFURequest(r *http.Request, req *SFURequest) (*SFUResponse, error) {
249 | // Note SFURequest has already been validated at this point
250 |
251 | userInfo, err := exchangeOpenIdUserInfo(r.Context(), req.OpenIDToken, h.skipVerifyTLS)
252 | if err != nil {
253 | return nil, &MatrixErrorResponse{
254 | Status: http.StatusUnauthorized,
255 | ErrCode: "M_UNAUTHORIZED",
256 | Err: "The request could not be authorised.",
257 | }
258 | }
259 |
260 | // Check if validated userInfo.Sub matches req.Member.ClaimedUserID
261 | if req.Member.ClaimedUserID != userInfo.Sub {
262 | log.Printf("Claimed user ID %s does not match token subject %s", req.Member.ClaimedUserID, userInfo.Sub)
263 | return nil, &MatrixErrorResponse{
264 | Status: http.StatusUnauthorized,
265 | ErrCode: "M_UNAUTHORIZED",
266 | Err: "The request could not be authorised.",
267 | }
268 | }
269 |
270 | // Does the user belong to homeservers granted full access
271 | isFullAccessUser := h.isFullAccessUser(req.OpenIDToken.MatrixServerName)
272 |
273 | log.Printf(
274 | "Got Matrix user info for %s (%s)",
275 | userInfo.Sub,
276 | map[bool]string{true: "full access", false: "restricted access"}[isFullAccessUser],
277 | )
278 |
279 | lkIdentity := req.Member.ID
280 | lkRoomAlias := fmt.Sprintf("%x", sha256.Sum256([]byte(req.RoomID + "|" + req.SlotID)))
281 | token, err := getJoinToken(h.key, h.secret, lkRoomAlias, lkIdentity)
282 | if err != nil {
283 | log.Printf("Error getting LiveKit token: %v", err)
284 | return nil, &MatrixErrorResponse{
285 | Status: http.StatusInternalServerError,
286 | ErrCode: "M_UNKNOWN",
287 | Err: "Internal Server Error",
288 | }
289 | }
290 |
291 | if isFullAccessUser {
292 | if err := createLiveKitRoom(r.Context(), h, lkRoomAlias, userInfo.Sub, lkIdentity); err != nil {
293 | return nil, &MatrixErrorResponse{
294 | Status: http.StatusInternalServerError,
295 | ErrCode: "M_UNKNOWN",
296 | Err: "Unable to create room on SFU",
297 | }
298 | }
299 | }
300 |
301 | return &SFUResponse{URL: h.lkUrl, JWT: token}, nil
302 | }
303 |
304 | var createLiveKitRoom = func(ctx context.Context, h *Handler, room, matrixUser, lkIdentity string) error {
305 | roomClient := lksdk.NewRoomServiceClient(h.lkUrl, h.key, h.secret)
306 | creationStart := time.Now().Unix()
307 | lkRoom, err := roomClient.CreateRoom(
308 | ctx,
309 | &livekit.CreateRoomRequest{
310 | Name: room,
311 | EmptyTimeout: 5 * 60, // 5 Minutes to keep the room open if no one joins
312 | DepartureTimeout: 20, // number of seconds to keep the room after everyone leaves
313 | MaxParticipants: 0, // 0 == no limitation
314 | },
315 | )
316 |
317 | if err != nil {
318 | return fmt.Errorf("unable to create room %s: %w", room, err)
319 | }
320 |
321 | // Log the room creation time and the user info
322 | isNewRoom := lkRoom.GetCreationTime() >= creationStart && lkRoom.GetCreationTime() <= time.Now().Unix()
323 | log.Printf(
324 | "%s LiveKit room sid: %s (alias: %s) for full-access Matrix user %s (LiveKit identity: %s)",
325 | map[bool]string{true: "Created", false: "Using"}[isNewRoom],
326 | lkRoom.Sid, room, matrixUser, lkIdentity,
327 | )
328 |
329 | return nil
330 | }
331 |
332 | func (h *Handler) prepareMux() *http.ServeMux {
333 |
334 | mux := http.NewServeMux()
335 | mux.HandleFunc("/sfu/get", h.handle_legacy) // TODO: This is deprecated and will be removed in future versions
336 | mux.HandleFunc("/get_token", h.handle)
337 | mux.HandleFunc("/healthz", h.healthcheck)
338 |
339 | return mux
340 | }
341 |
342 | func (h *Handler) healthcheck(w http.ResponseWriter, r *http.Request) {
343 | log.Printf("Health check from %s", r.RemoteAddr)
344 |
345 | if r.Method == "GET" {
346 | w.WriteHeader(http.StatusOK)
347 | return
348 | } else {
349 | w.WriteHeader(http.StatusMethodNotAllowed)
350 | }
351 | }
352 |
353 | // TODO: This is deprecated and will be removed in future versions
354 | func mapSFURequest(data *[]byte) (any, error) {
355 | requestTypes := []ValidatableSFURequest{&LegacySFURequest{}, &SFURequest{}}
356 | for _, req := range requestTypes {
357 | decoder := json.NewDecoder(strings.NewReader(string(*data)))
358 | decoder.DisallowUnknownFields()
359 | if err := decoder.Decode(req); err == nil {
360 | if err := req.Validate(); err != nil {
361 | return nil, err
362 | }
363 | return req, nil
364 | }
365 | }
366 |
367 | return nil, &MatrixErrorResponse{
368 | Status: http.StatusBadRequest,
369 | ErrCode: "M_BAD_JSON",
370 | Err: "The request body was malformed, missing required fields, or contained invalid values (e.g. missing `room_id`, `slot_id`, or `openid_token`).",
371 | }
372 | }
373 |
374 | // TODO: This is deprecated and will be removed in future versions
375 | func (h *Handler) handle_legacy(w http.ResponseWriter, r *http.Request) {
376 | log.Printf("Request from %s at \"%s\"", r.RemoteAddr, r.Header.Get("Origin"))
377 |
378 | w.Header().Set("Content-Type", "application/json")
379 |
380 | // Set the CORS headers
381 | w.Header().Set("Access-Control-Allow-Origin", "*")
382 | w.Header().Set("Access-Control-Allow-Methods", "POST")
383 | w.Header().Set("Access-Control-Allow-Headers", "Accept, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token")
384 |
385 | switch r.Method {
386 | case "OPTIONS":
387 | // Handle preflight request (CORS)
388 | w.WriteHeader(http.StatusOK)
389 | return
390 | case "POST":
391 | // Read request body once for later JSON parsing
392 | body, err := io.ReadAll(r.Body)
393 | if err != nil {
394 | log.Printf("Error reading request body: %v", err)
395 | writeMatrixError(w, http.StatusBadRequest, "M_NOT_JSON", "Error reading request")
396 | return
397 | }
398 |
399 | var sfuAccessResponse *SFUResponse
400 |
401 | sfuAccessRequest, err := mapSFURequest(&body)
402 | if err != nil {
403 | matrixErr := &MatrixErrorResponse{}
404 | if errors.As(err, &matrixErr) {
405 | log.Printf("Error processing request: %v", matrixErr.Err)
406 | writeMatrixError(w, matrixErr.Status, matrixErr.ErrCode, matrixErr.Err)
407 | return
408 | }
409 | }
410 |
411 | switch sfuReq := sfuAccessRequest.(type) {
412 | case *SFURequest:
413 | log.Printf("Processing SFU request")
414 | sfuAccessResponse, err = h.processSFURequest(r, sfuReq)
415 | case *LegacySFURequest:
416 | log.Printf("Processing legacy SFU request")
417 | sfuAccessResponse, err = h.processLegacySFURequest(r, sfuReq)
418 | }
419 |
420 | if err != nil {
421 | matrixErr := &MatrixErrorResponse{}
422 | if errors.As(err, &matrixErr) {
423 | log.Printf("Error processing request: %v", matrixErr.Err)
424 | writeMatrixError(w, matrixErr.Status, matrixErr.ErrCode, matrixErr.Err)
425 | return
426 | }
427 | }
428 |
429 | if err := json.NewEncoder(w).Encode(&sfuAccessResponse); err != nil {
430 | log.Printf("failed to encode json response! %v", err)
431 | }
432 | default:
433 | w.WriteHeader(http.StatusMethodNotAllowed)
434 | }
435 | }
436 |
437 | func (h *Handler) handle(w http.ResponseWriter, r *http.Request) {
438 | log.Printf("Request from %s at \"%s\"", r.RemoteAddr, r.Header.Get("Origin"))
439 |
440 | w.Header().Set("Content-Type", "application/json")
441 |
442 | // Set the CORS headers
443 | w.Header().Set("Access-Control-Allow-Origin", "*")
444 | w.Header().Set("Access-Control-Allow-Methods", "POST")
445 | w.Header().Set("Access-Control-Allow-Headers", "Accept, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token")
446 |
447 | // Handle preflight request (CORS)
448 | switch r.Method {
449 | case "OPTIONS":
450 | w.WriteHeader(http.StatusOK)
451 | return
452 | case "POST":
453 | var sfuAccessRequest SFURequest
454 |
455 | decoder := json.NewDecoder(r.Body)
456 | decoder.DisallowUnknownFields()
457 | if err := decoder.Decode(&sfuAccessRequest); err == nil {
458 | if err := sfuAccessRequest.Validate(); err != nil {
459 | matrixErr := &MatrixErrorResponse{}
460 | if errors.As(err, &matrixErr) {
461 | log.Printf("Error processing request: %v", matrixErr.Err)
462 | writeMatrixError(w, matrixErr.Status, matrixErr.ErrCode, matrixErr.Err)
463 | return
464 | }
465 | }
466 | } else {
467 | log.Printf("Error reading request body: %v", err)
468 | writeMatrixError(w, http.StatusBadRequest, "M_NOT_JSON", "Error reading request")
469 | return
470 | }
471 |
472 | log.Printf("Processing SFU request")
473 | sfuAccessResponse, err := h.processSFURequest(r, &sfuAccessRequest)
474 |
475 | if err != nil {
476 | matrixErr := &MatrixErrorResponse{}
477 | if errors.As(err, &matrixErr) {
478 | log.Printf("Error processing request: %v", matrixErr.Err)
479 | writeMatrixError(w, matrixErr.Status, matrixErr.ErrCode, matrixErr.Err)
480 | return
481 | }
482 | }
483 |
484 | if err := json.NewEncoder(w).Encode(&sfuAccessResponse); err != nil {
485 | log.Printf("failed to encode json response! %v", err)
486 | }
487 |
488 | default:
489 | w.WriteHeader(http.StatusMethodNotAllowed)
490 | }
491 | }
492 |
493 | func readKeySecret() (string, string) {
494 | // We initialize keys & secrets from environment variables
495 | key := os.Getenv("LIVEKIT_KEY")
496 | secret := os.Getenv("LIVEKIT_SECRET")
497 | // We initialize potential key & secret path from environment variables
498 | keyPath := os.Getenv("LIVEKIT_KEY_FROM_FILE")
499 | secretPath := os.Getenv("LIVEKIT_SECRET_FROM_FILE")
500 | keySecretPath := os.Getenv("LIVEKIT_KEY_FILE")
501 |
502 | // If keySecretPath is set we read the file and split it into two parts
503 | // It takes over any other initialization
504 | if keySecretPath != "" {
505 | if keySecretBytes, err := os.ReadFile(keySecretPath); err != nil {
506 | log.Fatal(err)
507 | } else {
508 | keySecrets := strings.Split(string(keySecretBytes), ":")
509 | if len(keySecrets) != 2 {
510 | log.Fatalf("invalid key secret file format!")
511 | }
512 | log.Printf("Using LiveKit API key and API secret from LIVEKIT_KEY_FILE")
513 | key = keySecrets[0]
514 | secret = keySecrets[1]
515 | }
516 | } else {
517 | // If keySecretPath is not set, we try to read the key and secret from files
518 | // If those files are not set, we return the key & secret from the environment variables
519 | if keyPath != "" {
520 | if keyBytes, err := os.ReadFile(keyPath); err != nil {
521 | log.Fatal(err)
522 | } else {
523 | log.Printf("Using LiveKit API key from LIVEKIT_KEY_FROM_FILE")
524 | key = string(keyBytes)
525 | }
526 | }
527 |
528 | if secretPath != "" {
529 | if secretBytes, err := os.ReadFile(secretPath); err != nil {
530 | log.Fatal(err)
531 | } else {
532 | log.Printf("Using LiveKit API secret from LIVEKIT_SECRET_FROM_FILE")
533 | secret = string(secretBytes)
534 | }
535 | }
536 |
537 | }
538 |
539 | // remove white spaces, new lines and carriage returns
540 | // from key and secret
541 | return strings.Trim(key, " \r\n"), strings.Trim(secret, " \r\n")
542 | }
543 |
544 | func parseConfig() (*Config, error) {
545 | skipVerifyTLS := os.Getenv("LIVEKIT_INSECURE_SKIP_VERIFY_TLS") == "YES_I_KNOW_WHAT_I_AM_DOING"
546 | if skipVerifyTLS {
547 | log.Printf("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!")
548 | log.Printf("!!! WARNING !!! LIVEKIT_INSECURE_SKIP_VERIFY_TLS !!! WARNING !!!")
549 | log.Printf("!!! WARNING !!! Allow to skip invalid TLS certificates !!! WARNING !!!")
550 | log.Printf("!!! WARNING !!! Use only for testing or debugging !!! WARNING !!!")
551 | log.Println("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!")
552 | }
553 |
554 | key, secret := readKeySecret()
555 | lkUrl := os.Getenv("LIVEKIT_URL")
556 |
557 | if key == "" || secret == "" || lkUrl == "" {
558 | return nil, fmt.Errorf("LIVEKIT_KEY[_FILE], LIVEKIT_SECRET[_FILE] and LIVEKIT_URL environment variables must be set")
559 | }
560 |
561 | fullAccessHomeservers := os.Getenv("LIVEKIT_FULL_ACCESS_HOMESERVERS")
562 |
563 | if len(fullAccessHomeservers) == 0 {
564 | localHomeservers := os.Getenv("LIVEKIT_LOCAL_HOMESERVERS")
565 | if len(localHomeservers) > 0 {
566 | log.Printf("!!! LIVEKIT_LOCAL_HOMESERVERS is deprecated, please use LIVEKIT_FULL_ACCESS_HOMESERVERS instead !!!")
567 | fullAccessHomeservers = localHomeservers
568 | } else {
569 | log.Printf("LIVEKIT_FULL_ACCESS_HOMESERVERS not set, defaulting to wildcard (*) for full access")
570 | fullAccessHomeservers = "*"
571 | }
572 | }
573 |
574 | lkJwtBind := os.Getenv("LIVEKIT_JWT_BIND")
575 | lkJwtPort := os.Getenv("LIVEKIT_JWT_PORT")
576 |
577 | if lkJwtBind == "" {
578 | if lkJwtPort == "" {
579 | lkJwtPort = "8080"
580 | } else {
581 | log.Printf("!!! LIVEKIT_JWT_PORT is deprecated, please use LIVEKIT_JWT_BIND instead !!!")
582 | }
583 | lkJwtBind = fmt.Sprintf(":%s", lkJwtPort)
584 | } else if lkJwtPort != "" {
585 | return nil, fmt.Errorf("LIVEKIT_JWT_BIND and LIVEKIT_JWT_PORT environment variables MUST NOT be set together")
586 | }
587 |
588 | return &Config{
589 | Key: key,
590 | Secret: secret,
591 | LkUrl: lkUrl,
592 | SkipVerifyTLS: skipVerifyTLS,
593 | FullAccessHomeservers: strings.Fields(strings.ReplaceAll(fullAccessHomeservers, ",", " ")),
594 | LkJwtBind: lkJwtBind,
595 | }, nil
596 | }
597 |
598 | func main() {
599 | config, err := parseConfig()
600 | if err != nil {
601 | log.Fatal(err)
602 | }
603 |
604 | log.Printf("LIVEKIT_URL: %s, LIVEKIT_JWT_BIND: %s", config.LkUrl, config.LkJwtBind)
605 | log.Printf("LIVEKIT_FULL_ACCESS_HOMESERVERS: %v", config.FullAccessHomeservers)
606 |
607 | handler := &Handler{
608 | key: config.Key,
609 | secret: config.Secret,
610 | lkUrl: config.LkUrl,
611 | skipVerifyTLS: config.SkipVerifyTLS,
612 | fullAccessHomeservers: config.FullAccessHomeservers,
613 | }
614 |
615 | log.Fatal(http.ListenAndServe(config.LkJwtBind, handler.prepareMux()))
616 | }
617 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.0-20241127180247-a33202765966.1 h1:ntAj16eF7AtUyzOOAFk5gvbAO52QmUKPKk7GmsIEORo=
2 | buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.0-20241127180247-a33202765966.1/go.mod h1:AxRT+qTj5PJCz2nyQzsR/qxAcveW5USRhJTt/edTO5w=
3 | buf.build/go/protoyaml v0.3.1 h1:ucyzE7DRnjX+mQ6AH4JzN0Kg50ByHHu+yrSKbgQn2D4=
4 | buf.build/go/protoyaml v0.3.1/go.mod h1:0TzNpFQDXhwbkXb/ajLvxIijqbve+vMQvWY/b3/Dzxg=
5 | cel.dev/expr v0.19.0 h1:lXuo+nDhpyJSpWxpPVi5cPUwzKb+dsdOiw6IreM5yt0=
6 | cel.dev/expr v0.19.0/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw=
7 | dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
8 | dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
9 | github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0=
10 | github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
11 | github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
12 | github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
13 | github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw=
14 | github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk=
15 | github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI=
16 | github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g=
17 | github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o=
18 | github.com/benbjohnson/clock v1.3.5/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
19 | github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY=
20 | github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0=
21 | github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
22 | github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
23 | github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
24 | github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
25 | github.com/bufbuild/protovalidate-go v0.8.0 h1:Xs3kCLCJ4tQiogJ0iOXm+ClKw/KviW3nLAryCGW2I3Y=
26 | github.com/bufbuild/protovalidate-go v0.8.0/go.mod h1:JPWZInGm2y2NBg3vKDKdDIkvDjyLv31J3hLH5GIFc/Q=
27 | github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
28 | github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
29 | github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
30 | github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
31 | github.com/containerd/continuity v0.4.3 h1:6HVkalIp+2u1ZLH1J/pYX2oBVXlJZvh1X1A7bEZ9Su8=
32 | github.com/containerd/continuity v0.4.3/go.mod h1:F6PTNCKepoxEaXLQp3wDAjygEnImnZ/7o4JzpodfroQ=
33 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
34 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
35 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
36 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
37 | github.com/dennwc/iters v1.0.1 h1:XwMudE6xtS0ugEdum4HQ+iRi+5HSvaeKxJPM/VI3pJs=
38 | github.com/dennwc/iters v1.0.1/go.mod h1:M9KuuMBeyEXYTmB7EnI9SCyALFCmPWOIxn5W1L0CjGg=
39 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
40 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
41 | github.com/docker/cli v26.1.4+incompatible h1:I8PHdc0MtxEADqYJZvhBrW9bo8gawKwwenxRM7/rLu8=
42 | github.com/docker/cli v26.1.4+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
43 | github.com/docker/docker v27.1.1+incompatible h1:hO/M4MtV36kzKldqnA37IWhebRA+LnqqcqDja6kVaKY=
44 | github.com/docker/docker v27.1.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
45 | github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
46 | github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
47 | github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
48 | github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
49 | github.com/envoyproxy/protoc-gen-validate v1.1.0 h1:tntQDh69XqOCOZsDz0lVJQez/2L6Uu2PdjCQwWCJ3bM=
50 | github.com/envoyproxy/protoc-gen-validate v1.1.0/go.mod h1:sXRDRVmzEbkM7CVcM06s9shE/m23dg3wzjl0UWqJ2q4=
51 | github.com/frostbyte73/core v0.1.1 h1:ChhJOR7bAKOCPbA+lqDLE2cGKlCG5JXsDvvQr4YaJIA=
52 | github.com/frostbyte73/core v0.1.1/go.mod h1:mhfOtR+xWAvwXiwor7jnqPMnu4fxbv1F2MwZ0BEpzZo=
53 | github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
54 | github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
55 | github.com/gammazero/deque v1.0.0 h1:LTmimT8H7bXkkCy6gZX7zNLtkbz4NdS2z8LZuor3j34=
56 | github.com/gammazero/deque v1.0.0/go.mod h1:iflpYvtGfM3U8S8j+sZEKIak3SAKYpA5/SQewgfXDKo=
57 | github.com/go-jose/go-jose/v3 v3.0.4 h1:Wp5HA7bLQcKnf6YYao/4kpRpVMp/yf6+pJKV8WFSaNY=
58 | github.com/go-jose/go-jose/v3 v3.0.4/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ=
59 | github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
60 | github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
61 | github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
62 | github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
63 | github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
64 | github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
65 | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
66 | github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
67 | github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
68 | github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
69 | github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
70 | github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
71 | github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
72 | github.com/google/cel-go v0.22.1 h1:AfVXx3chM2qwoSbM7Da8g8hX8OVSkBFwX+rz2+PcK40=
73 | github.com/google/cel-go v0.22.1/go.mod h1:BuznPXXfQDpXKWQ9sPW3TzlAJN5zzFe+i9tIs0yC4s8=
74 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
75 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
76 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
77 | github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
78 | github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
79 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
80 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
81 | github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
82 | github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
83 | github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw=
84 | github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI=
85 | github.com/hashicorp/go-set/v3 v3.0.0 h1:CaJBQvQCOWoftrBcDt7Nwgo0kdpmrKxar/x2o6pV9JA=
86 | github.com/hashicorp/go-set/v3 v3.0.0/go.mod h1:IEghM2MpE5IaNvL+D7X480dfNtxjRXZ6VMpK3C8s2ok=
87 | github.com/jxskiss/base62 v1.1.0 h1:A5zbF8v8WXx2xixnAKD2w+abC+sIzYJX+nxmhA6HWFw=
88 | github.com/jxskiss/base62 v1.1.0/go.mod h1:HhWAlUXvxKThfOlZbcuFzsqwtF5TcqS9ru3y5GfjWAc=
89 | github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
90 | github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
91 | github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
92 | github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
93 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
94 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
95 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
96 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
97 | github.com/lithammer/shortuuid/v4 v4.2.0 h1:LMFOzVB3996a7b8aBuEXxqOBflbfPQAiVzkIcHO0h8c=
98 | github.com/lithammer/shortuuid/v4 v4.2.0/go.mod h1:D5noHZ2oFw/YaKCfGy0YxyE7M0wMbezmMjPdhyEFe6Y=
99 | github.com/livekit/mageutil v0.0.0-20230125210925-54e8a70427c1 h1:jm09419p0lqTkDaKb5iXdynYrzB84ErPPO4LbRASk58=
100 | github.com/livekit/mageutil v0.0.0-20230125210925-54e8a70427c1/go.mod h1:Rs3MhFwutWhGwmY1VQsygw28z5bWcnEYmS1OG9OxjOQ=
101 | github.com/livekit/mediatransportutil v0.0.0-20241220010243-a2bdee945564 h1:GX7KF/V9ExmcfT/2Bdia8aROjkxrgx7WpyH7w9MB4J4=
102 | github.com/livekit/mediatransportutil v0.0.0-20241220010243-a2bdee945564/go.mod h1:36s+wwmU3O40IAhE+MjBWP3W71QRiEE9SfooSBvtBqY=
103 | github.com/livekit/protocol v1.34.0 h1:hbIXgNW+JPiTcGjzNg1XgQg3Wqa2R5dBhzuy+LLEIS4=
104 | github.com/livekit/protocol v1.34.0/go.mod h1:yXuQ7ucrLj91nbxL6/AHgtxdha1DGzLj1LkgvnT90So=
105 | github.com/livekit/psrpc v0.6.1-0.20250205181828-a0beed2e4126 h1:fzuYpAQbCid7ySPpQWWePfQOWUrs8x6dJ0T3Wl07n+Y=
106 | github.com/livekit/psrpc v0.6.1-0.20250205181828-a0beed2e4126/go.mod h1:X5WtEZ7OnEs72Fi5/J+i0on3964F1aynQpCalcgMqRo=
107 | github.com/livekit/server-sdk-go/v2 v2.5.0 h1:HCKm3f6PvefGp8emNC2mi9+9IXzBYrynuGbtUdp5u+w=
108 | github.com/livekit/server-sdk-go/v2 v2.5.0/go.mod h1:98/Sa+Wgb27ABwu0WYxLaMZaRfGljrrtoZDQ2xA4oVg=
109 | github.com/magefile/mage v1.15.0 h1:BvGheCMAsG3bWUDbZ8AyXXpCNwU9u5CB6sM+HNb9HYg=
110 | github.com/magefile/mage v1.15.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
111 | github.com/matrix-org/gomatrix v0.0.0-20220926102614-ceba4d9f7530 h1:kHKxCOLcHH8r4Fzarl4+Y3K5hjothkVW5z7T1dUM11U=
112 | github.com/matrix-org/gomatrix v0.0.0-20220926102614-ceba4d9f7530/go.mod h1:/gBX06Kw0exX1HrwmoBibFA98yBk/jxKpGVeyQbff+s=
113 | github.com/matrix-org/gomatrixserverlib v0.0.0-20250619052822-904c8f04597e h1:SWediqisy1Eoumr06sjGaA6gt6gS4FtXe00VB6fSNZw=
114 | github.com/matrix-org/gomatrixserverlib v0.0.0-20250619052822-904c8f04597e/go.mod h1:61LpEsWAroRfdVh2dnr6fQ+K3MmRgD5I35GVvF4FpXQ=
115 | github.com/matrix-org/gomatrixserverlib v0.0.0-20250704071233-a234d6df21c7 h1:WAcUwx+ZCK8znn1etraC2JWTns3ppcH6/gVQLfrCAnI=
116 | github.com/matrix-org/gomatrixserverlib v0.0.0-20250704071233-a234d6df21c7/go.mod h1:61LpEsWAroRfdVh2dnr6fQ+K3MmRgD5I35GVvF4FpXQ=
117 | github.com/matrix-org/gomatrixserverlib v0.0.0-20250815065806-6697d93cbcba h1:vUUjTOXZ/bYdF/SmJPH8HZ/UTmvw+ldngFKVLElmn+I=
118 | github.com/matrix-org/gomatrixserverlib v0.0.0-20250815065806-6697d93cbcba/go.mod h1:b6KVfDjXjA5Q7vhpOaMqIhFYvu5BuFVZixlNeTV/CLc=
119 | github.com/matrix-org/util v0.0.0-20221111132719-399730281e66 h1:6z4KxomXSIGWqhHcfzExgkH3Z3UkIXry4ibJS4Aqz2Y=
120 | github.com/matrix-org/util v0.0.0-20221111132719-399730281e66/go.mod h1:iBI1foelCqA09JJgPV0FYz4qA5dUXYOxMi57FxKBdd4=
121 | github.com/miekg/dns v1.1.66 h1:FeZXOS3VCVsKnEAd+wBkjMC3D2K+ww66Cq3VnCINuJE=
122 | github.com/miekg/dns v1.1.66/go.mod h1:jGFzBsSNbJw6z1HYut1RKBKHA9PBdxeHrZG8J+gC2WE=
123 | github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
124 | github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
125 | github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
126 | github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
127 | github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
128 | github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
129 | github.com/nats-io/nats.go v1.38.0 h1:A7P+g7Wjp4/NWqDOOP/K6hfhr54DvdDQUznt5JFg9XA=
130 | github.com/nats-io/nats.go v1.38.0/go.mod h1:IGUM++TwokGnXPs82/wCuiHS02/aKrdYUQkU8If6yjw=
131 | github.com/nats-io/nkeys v0.4.9 h1:qe9Faq2Gxwi6RZnZMXfmGMZkg3afLLOtrU+gDZJ35b0=
132 | github.com/nats-io/nkeys v0.4.9/go.mod h1:jcMqs+FLG+W5YO36OX6wFIFcmpdAns+w1Wm6D3I/evE=
133 | github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
134 | github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
135 | github.com/oleiade/lane/v2 v2.0.0 h1:XW/ex/Inr+bPkLd3O240xrFOhUkTd4Wy176+Gv0E3Qw=
136 | github.com/oleiade/lane/v2 v2.0.0/go.mod h1:i5FBPFAYSWCgLh58UkUGCChjcCzef/MI7PlQm2TKCeg=
137 | github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
138 | github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
139 | github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
140 | github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
141 | github.com/opencontainers/runc v1.1.13 h1:98S2srgG9vw0zWcDpFMn5TRrh8kLxa/5OFUstuUhmRs=
142 | github.com/opencontainers/runc v1.1.13/go.mod h1:R016aXacfp/gwQBYw2FDGa9m+n6atbLWrYY8hNMT/sA=
143 | github.com/ory/dockertest/v3 v3.11.0 h1:OiHcxKAvSDUwsEVh2BjxQQc/5EHz9n0va9awCtNGuyA=
144 | github.com/ory/dockertest/v3 v3.11.0/go.mod h1:VIPxS1gwT9NpPOrfD3rACs8Y9Z7yhzO4SB194iUDnUI=
145 | github.com/pion/datachannel v1.5.10 h1:ly0Q26K1i6ZkGf42W7D4hQYR90pZwzFOjTq5AuCKk4o=
146 | github.com/pion/datachannel v1.5.10/go.mod h1:p/jJfC9arb29W7WrxyKbepTU20CFgyx5oLo8Rs4Py/M=
147 | github.com/pion/dtls/v3 v3.0.4 h1:44CZekewMzfrn9pmGrj5BNnTMDCFwr+6sLH+cCuLM7U=
148 | github.com/pion/dtls/v3 v3.0.4/go.mod h1:R373CsjxWqNPf6MEkfdy3aSe9niZvL/JaKlGeFphtMg=
149 | github.com/pion/ice/v4 v4.0.6 h1:jmM9HwI9lfetQV/39uD0nY4y++XZNPhvzIPCb8EwxUM=
150 | github.com/pion/ice/v4 v4.0.6/go.mod h1:y3M18aPhIxLlcO/4dn9X8LzLLSma84cx6emMSu14FGw=
151 | github.com/pion/interceptor v0.1.39 h1:Y6k0bN9Y3Lg/Wb21JBWp480tohtns8ybJ037AGr9UuA=
152 | github.com/pion/interceptor v0.1.39/go.mod h1:Z6kqH7M/FYirg3frjGJ21VLSRJGBXB/KqaTIrdqnOic=
153 | github.com/pion/logging v0.2.3 h1:gHuf0zpoh1GW67Nr6Gj4cv5Z9ZscU7g/EaoC/Ke/igI=
154 | github.com/pion/logging v0.2.3/go.mod h1:z8YfknkquMe1csOrxK5kc+5/ZPAzMxbKLX5aXpbpC90=
155 | github.com/pion/mdns/v2 v2.0.7 h1:c9kM8ewCgjslaAmicYMFQIde2H9/lrZpjBkN8VwoVtM=
156 | github.com/pion/mdns/v2 v2.0.7/go.mod h1:vAdSYNAT0Jy3Ru0zl2YiW3Rm/fJCwIeM0nToenfOJKA=
157 | github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
158 | github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
159 | github.com/pion/rtcp v1.2.15 h1:LZQi2JbdipLOj4eBjK4wlVoQWfrZbh3Q6eHtWtJBZBo=
160 | github.com/pion/rtcp v1.2.15/go.mod h1:jlGuAjHMEXwMUHK78RgX0UmEJFV4zUKOFHR7OP+D3D0=
161 | github.com/pion/rtp v1.8.18 h1:yEAb4+4a8nkPCecWzQB6V/uEU18X1lQCGAQCjP+pyvU=
162 | github.com/pion/rtp v1.8.18/go.mod h1:bAu2UFKScgzyFqvUKmbvzSdPr+NGbZtv6UB2hesqXBk=
163 | github.com/pion/sctp v1.8.35 h1:qwtKvNK1Wc5tHMIYgTDJhfZk7vATGVHhXbUDfHbYwzA=
164 | github.com/pion/sctp v1.8.35/go.mod h1:EcXP8zCYVTRy3W9xtOF7wJm1L1aXfKRQzaM33SjQlzg=
165 | github.com/pion/sdp/v3 v3.0.10 h1:6MChLE/1xYB+CjumMw+gZ9ufp2DPApuVSnDT8t5MIgA=
166 | github.com/pion/sdp/v3 v3.0.10/go.mod h1:88GMahN5xnScv1hIMTqLdu/cOcUkj6a9ytbncwMCq2E=
167 | github.com/pion/srtp/v3 v3.0.4 h1:2Z6vDVxzrX3UHEgrUyIGM4rRouoC7v+NiF1IHtp9B5M=
168 | github.com/pion/srtp/v3 v3.0.4/go.mod h1:1Jx3FwDoxpRaTh1oRV8A/6G1BnFL+QI82eK4ms8EEJQ=
169 | github.com/pion/stun/v3 v3.0.0 h1:4h1gwhWLWuZWOJIJR9s2ferRO+W3zA/b6ijOI6mKzUw=
170 | github.com/pion/stun/v3 v3.0.0/go.mod h1:HvCN8txt8mwi4FBvS3EmDghW6aQJ24T+y+1TKjB5jyU=
171 | github.com/pion/transport/v3 v3.0.7 h1:iRbMH05BzSNwhILHoBoAPxoB9xQgOaJk+591KC9P1o0=
172 | github.com/pion/transport/v3 v3.0.7/go.mod h1:YleKiTZ4vqNxVwh77Z0zytYi7rXHl7j6uPLGhhz9rwo=
173 | github.com/pion/turn/v4 v4.0.0 h1:qxplo3Rxa9Yg1xXDxxH8xaqcyGUtbHYw4QSCvmFWvhM=
174 | github.com/pion/turn/v4 v4.0.0/go.mod h1:MuPDkm15nYSklKpN8vWJ9W2M0PlyQZqYt1McGuxG7mA=
175 | github.com/pion/webrtc/v4 v4.0.9 h1:PyOYMRKJgfy0dzPcYtFD/4oW9zaw3Ze3oZzzbj2LV9E=
176 | github.com/pion/webrtc/v4 v4.0.9/go.mod h1:ViHLVaNpiuvaH8pdiuQxuA9awuE6KVzAXx3vVWilOck=
177 | github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
178 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
179 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
180 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
181 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
182 | github.com/puzpuzpuz/xsync/v3 v3.5.0 h1:i+cMcpEDY1BkNm7lPDkCtE4oElsYLn+EKF8kAu2vXT4=
183 | github.com/puzpuzpuz/xsync/v3 v3.5.0/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
184 | github.com/redis/go-redis/v9 v9.7.3 h1:YpPyAayJV+XErNsatSElgRZZVCwXX9QzkKYNvO7x0wM=
185 | github.com/redis/go-redis/v9 v9.7.3/go.mod h1:bGUrSggJ9X9GUmZpZNEOQKaANxSGgOEBRltRTZHSvrA=
186 | github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
187 | github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
188 | github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
189 | github.com/shoenig/test v1.7.0 h1:eWcHtTXa6QLnBvm0jgEabMRN/uJ4DMV3M8xUGgRkZmk=
190 | github.com/shoenig/test v1.7.0/go.mod h1:UxJ6u/x2v/TNs/LoLxBNJRV9DiwBBKYxXSyczsBHFoI=
191 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
192 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
193 | github.com/stoewer/go-strcase v1.3.0 h1:g0eASXYtp+yvN9fK8sH94oCIk0fau9uV1/ZdJ0AVEzs=
194 | github.com/stoewer/go-strcase v1.3.0/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo=
195 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
196 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
197 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
198 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
199 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
200 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
201 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
202 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
203 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
204 | github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
205 | github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
206 | github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
207 | github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
208 | github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
209 | github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
210 | github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
211 | github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
212 | github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
213 | github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
214 | github.com/twitchtv/twirp v8.1.3+incompatible h1:+F4TdErPgSUbMZMwp13Q/KgDVuI7HJXP61mNV3/7iuU=
215 | github.com/twitchtv/twirp v8.1.3+incompatible/go.mod h1:RRJoFSAmTEh2weEqWtpPE3vFK5YBhA6bqp2l1kfCC5A=
216 | github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU=
217 | github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
218 | github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo=
219 | github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
220 | github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
221 | github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
222 | github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
223 | github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
224 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
225 | github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ=
226 | github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
227 | github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0=
228 | github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA=
229 | go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
230 | go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
231 | go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
232 | go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
233 | go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
234 | go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
235 | go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
236 | go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
237 | go.uber.org/zap/exp v0.3.0 h1:6JYzdifzYkGmTdRR59oYH+Ng7k49H9qVpWwNSsGJj3U=
238 | go.uber.org/zap/exp v0.3.0/go.mod h1:5I384qq7XGxYyByIhHm6jg5CHkGY0nsTfbDLgDDlgJQ=
239 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
240 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
241 | golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
242 | golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
243 | golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
244 | golang.org/x/exp v0.0.0-20250207012021-f9890c6ad9f3 h1:qNgPs5exUA+G0C96DrPwNrvLSj7GT/9D+3WMWUcUg34=
245 | golang.org/x/exp v0.0.0-20250207012021-f9890c6ad9f3/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU=
246 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
247 | golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
248 | golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
249 | golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
250 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
251 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
252 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
253 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
254 | golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
255 | golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
256 | golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
257 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
258 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
259 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
260 | golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
261 | golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
262 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
263 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
264 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
265 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
266 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
267 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
268 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
269 | golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
270 | golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
271 | golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
272 | golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
273 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
274 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
275 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
276 | golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
277 | golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
278 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
279 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
280 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
281 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
282 | golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
283 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
284 | golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
285 | golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
286 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
287 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
288 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
289 | golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
290 | golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc=
291 | golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI=
292 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
293 | google.golang.org/genproto/googleapis/api v0.0.0-20241202173237-19429a94021a h1:OAiGFfOiA0v9MRYsSidp3ubZaBnteRUyn3xB2ZQ5G/E=
294 | google.golang.org/genproto/googleapis/api v0.0.0-20241202173237-19429a94021a/go.mod h1:jehYqy3+AhJU9ve55aNOaSml7wUXjF9x6z2LcCfpAhY=
295 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250204164813-702378808489 h1:5bKytslY8ViY0Cj/ewmRtrWHW64bNF03cAatUUFCdFI=
296 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250204164813-702378808489/go.mod h1:8BS3B93F/U1juMFq9+EDk+qOT5CO1R9IzXxG3PTqiRk=
297 | google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ=
298 | google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw=
299 | google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
300 | google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
301 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
302 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
303 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
304 | gopkg.in/h2non/gock.v1 v1.1.2 h1:jBbHXgGBK/AoPVfJh5x4r/WxIrElvbLel8TCZkkZJoY=
305 | gopkg.in/h2non/gock.v1 v1.1.2/go.mod h1:n7UGz/ckNChHiK05rDoiC4MYSunEC/lyaUm2WWaDva0=
306 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
307 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
308 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
309 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
310 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
311 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | GNU AFFERO GENERAL PUBLIC LICENSE
2 | Version 3, 19 November 2007
3 |
4 | Copyright (C) 2007 Free Software Foundation, Inc.