diff --git a/go.mod b/go.mod
index dd60c36b..a064e920 100644
--- a/go.mod
+++ b/go.mod
@@ -34,7 +34,7 @@ require (
github.com/go-gst/go-glib v1.4.0
github.com/go-gst/go-gst v1.4.0
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0
- github.com/golang/glog v1.2.4
+ github.com/golang/glog v1.2.5
github.com/golangci/golangci-lint/v2 v2.1.6
github.com/google/uuid v1.6.0
github.com/gorilla/websocket v1.5.3
@@ -57,7 +57,7 @@ require (
github.com/pion/rtcp v1.2.16
github.com/pion/webrtc/v4 v4.0.11
github.com/piprate/json-gold v0.5.0
- github.com/prometheus/client_golang v1.23.0
+ github.com/prometheus/client_golang v1.23.2
github.com/rivo/uniseg v0.4.7
github.com/rs/cors v1.11.1
github.com/samber/slog-http v1.4.0
@@ -69,20 +69,21 @@ require (
github.com/stretchr/testify v1.11.1
github.com/tdewolff/canvas v0.0.0-20250728095813-50d4cb1eee71
github.com/tetratelabs/wazero v1.11.0
+ github.com/tus/tusd/v2 v2.8.0
github.com/urfave/cli/v3 v3.6.2
github.com/whyrusleeping/cbor-gen v0.3.1
github.com/whyrusleeping/go-did v0.0.0-20230824162731-404d1707d5d6
gitlab.com/gitlab-org/release-cli v0.18.0
- go.opentelemetry.io/otel v1.36.0
+ go.opentelemetry.io/otel v1.39.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.33.0
- go.opentelemetry.io/otel/sdk v1.36.0
- go.opentelemetry.io/otel/trace v1.36.0
+ go.opentelemetry.io/otel/sdk v1.39.0
+ go.opentelemetry.io/otel/trace v1.39.0
go.uber.org/goleak v1.3.0
golang.org/x/image v0.30.0
golang.org/x/net v0.52.0
golang.org/x/sync v0.20.0
golang.org/x/term v0.41.0
- golang.org/x/time v0.12.0
+ golang.org/x/time v0.14.0
golang.org/x/tools v0.42.0
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da
google.golang.org/api v0.228.0
@@ -110,14 +111,16 @@ require (
require (
4d63.com/gocheckcompilerdirectives v1.3.0 // indirect
4d63.com/gochecknoglobals v0.2.2 // indirect
- cloud.google.com/go v0.116.0 // indirect
- cloud.google.com/go/auth v0.15.0 // indirect
+ cel.dev/expr v0.25.1 // indirect
+ cloud.google.com/go v0.120.1 // indirect
+ cloud.google.com/go/auth v0.16.1 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
- cloud.google.com/go/compute/metadata v0.6.0 // indirect
- cloud.google.com/go/firestore v1.16.0 // indirect
- cloud.google.com/go/iam v1.2.0 // indirect
- cloud.google.com/go/longrunning v0.6.0 // indirect
- cloud.google.com/go/storage v1.43.0 // indirect
+ cloud.google.com/go/compute/metadata v0.9.0 // indirect
+ cloud.google.com/go/firestore v1.18.0 // indirect
+ cloud.google.com/go/iam v1.5.1 // indirect
+ cloud.google.com/go/longrunning v0.6.6 // indirect
+ cloud.google.com/go/monitoring v1.24.1 // indirect
+ cloud.google.com/go/storage v1.51.0 // indirect
codeberg.org/go-latex/latex v0.1.0 // indirect
codeberg.org/go-pdf/fpdf v0.11.1 // indirect
contrib.go.opencensus.io/exporter/prometheus v0.4.2 // indirect
@@ -138,6 +141,9 @@ require (
github.com/ByteArena/poly2tri-go v0.0.0-20170716161910-d102ad91854f // indirect
github.com/Djarvur/go-err113 v0.0.0-20210108212216-aea10b59be24 // indirect
github.com/GaijinEntertainment/go-exhaustruct/v3 v3.3.1 // indirect
+ github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0 // indirect
+ github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.51.0 // indirect
+ github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0 // indirect
github.com/Kagami/go-avif v0.1.0 // indirect
github.com/Masterminds/semver/v3 v3.3.1 // indirect
github.com/MicahParks/keyfunc v1.9.0 // indirect
@@ -199,6 +205,7 @@ require (
github.com/chavacava/garif v0.1.0 // indirect
github.com/ckaznocha/intrange v0.3.1 // indirect
github.com/cloudflare/circl v1.3.7 // indirect
+ github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 // indirect
github.com/consensys/gnark-crypto v0.18.0 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
github.com/crate-crypto/go-eth-kzg v1.3.0 // indirect
@@ -222,6 +229,8 @@ require (
github.com/earthboundkid/versioninfo/v2 v2.24.1 // indirect
github.com/emicklei/dot v1.9.0 // indirect
github.com/emirpasic/gods v1.18.1 // indirect
+ github.com/envoyproxy/go-control-plane/envoy v1.36.0 // indirect
+ github.com/envoyproxy/protoc-gen-validate v1.3.0 // indirect
github.com/ethereum/c-kzg-4844/v2 v2.1.1 // indirect
github.com/ethereum/go-verkle v0.2.2 // indirect
github.com/ettle/strcase v0.2.0 // indirect
@@ -239,6 +248,7 @@ require (
github.com/go-fonts/latin-modern v0.3.3 // indirect
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
github.com/go-git/go-billy/v5 v5.5.0 // indirect
+ github.com/go-jose/go-jose/v4 v4.1.3 // 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.3 // indirect
@@ -264,7 +274,7 @@ require (
github.com/gofrs/flock v0.12.1 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang-jwt/jwt/v4 v4.5.2 // indirect
- github.com/golang-jwt/jwt/v5 v5.2.2 // indirect
+ github.com/golang-jwt/jwt/v5 v5.3.0 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/mock v1.6.0 // indirect
github.com/golang/protobuf v1.5.4 // indirect
@@ -279,7 +289,7 @@ require (
github.com/golangci/unconvert v0.0.0-20250410112200-a129a6e6413e // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/google/s2a-go v0.1.9 // indirect
- github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect
+ github.com/googleapis/enterprise-certificate-proxy v0.3.11 // indirect
github.com/googleapis/gax-go/v2 v2.14.1 // indirect
github.com/gordonklaus/ineffassign v0.1.0 // indirect
github.com/gorilla/mux v1.8.1 // indirect
@@ -287,7 +297,7 @@ require (
github.com/gostaticanalysis/comment v1.5.0 // indirect
github.com/gostaticanalysis/forcetypeassert v0.2.0 // indirect
github.com/gostaticanalysis/nilerr v0.1.1 // indirect
- github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1 // indirect
+ github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 // indirect
github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
@@ -429,11 +439,12 @@ require (
github.com/pion/turn/v4 v4.0.0 // indirect
github.com/pjbgf/sha1cd v0.3.0 // indirect
github.com/pkg/errors v0.9.1 // indirect
+ github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f // indirect
github.com/polyfloyd/go-errorlint v1.8.0 // indirect
github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
- github.com/prometheus/common v0.65.0 // indirect
+ github.com/prometheus/common v0.66.1 // indirect
github.com/prometheus/procfs v0.17.0 // indirect
github.com/prometheus/statsd_exporter v0.27.1 // indirect
github.com/quasilyte/go-ruleguard v0.4.4 // indirect
@@ -445,7 +456,7 @@ require (
github.com/rabbitmq/rabbitmq-stream-go-client v1.1.1 // indirect
github.com/raeperd/recvcheck v0.2.0 // indirect
github.com/rogpeppe/go-internal v1.14.1 // indirect
- github.com/rs/xid v1.5.0 // indirect
+ github.com/rs/xid v1.6.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/ryancurrah/gomodguard v1.4.1 // indirect
github.com/ryanrolds/sqlclosecheck v0.5.1 // indirect
@@ -471,6 +482,7 @@ require (
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/pflag v1.0.6 // indirect
github.com/spf13/viper v1.12.0 // indirect
+ github.com/spiffe/go-spiffe/v2 v2.6.0 // indirect
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef // indirect
github.com/srwiley/scanx v0.0.0-20190309010443-e94503791388 // indirect
github.com/ssgreg/nlreturn/v2 v2.2.1 // indirect
@@ -517,30 +529,33 @@ require (
go-simpler.org/sloglint v0.11.0 // indirect
go.augendre.info/fatcontext v0.8.0 // indirect
go.opencensus.io v0.24.0 // indirect
- go.opentelemetry.io/auto/sdk v1.1.0 // indirect
- go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0 // indirect
+ go.opentelemetry.io/auto/sdk v1.2.1 // indirect
+ go.opentelemetry.io/contrib/detectors/gcp v1.39.0 // indirect
+ go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 // indirect
- go.opentelemetry.io/otel/metric v1.36.0 // indirect
- go.opentelemetry.io/proto/otlp v1.5.0 // indirect
+ go.opentelemetry.io/otel/metric v1.39.0 // indirect
+ go.opentelemetry.io/otel/sdk/metric v1.39.0 // indirect
+ go.opentelemetry.io/proto/otlp v1.7.1 // indirect
go.uber.org/atomic v1.11.0 // indirect
go.uber.org/automaxprocs v1.6.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.27.0 // indirect
+ go.yaml.in/yaml/v2 v2.4.2 // indirect
golang.org/x/crypto v0.49.0 // indirect
golang.org/x/exp v0.0.0-20250813145105-42675adae3e6 // indirect
golang.org/x/exp/typeparams v0.0.0-20250210185358-939b2ce775ac // indirect
golang.org/x/mod v0.33.0 // indirect
- golang.org/x/oauth2 v0.30.0 // indirect
+ golang.org/x/oauth2 v0.35.0 // indirect
golang.org/x/telemetry v0.0.0-20260209163413-e7419c687ee4 // indirect
golang.org/x/text v0.35.0 // indirect
gonum.org/v1/plot v0.16.0 // indirect
google.golang.org/appengine/v2 v2.0.2 // indirect
- google.golang.org/genproto v0.0.0-20240903143218-8af14fe29dc1 // indirect
- google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a // indirect
- google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4 // indirect
- google.golang.org/grpc v1.71.0 // indirect
- google.golang.org/protobuf v1.36.7 // indirect
+ google.golang.org/genproto v0.0.0-20250303144028-a0af3efb3deb // indirect
+ google.golang.org/genproto/googleapis/api v0.0.0-20260203192932-546029d2fa20 // indirect
+ google.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20 // indirect
+ google.golang.org/grpc v1.79.1 // indirect
+ google.golang.org/protobuf v1.36.11 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
diff --git a/go.sum b/go.sum
index ef63c199..7099246d 100644
--- a/go.sum
+++ b/go.sum
@@ -2,6 +2,8 @@
4d63.com/gocheckcompilerdirectives v1.3.0/go.mod h1:ofsJ4zx2QAuIP/NO/NAh1ig6R1Fb18/GI7RVMwz7kAY=
4d63.com/gochecknoglobals v0.2.2 h1:H1vdnwnMaZdQW/N+NrkT1SZMTBmcwHe9Vq8lJcYYTtU=
4d63.com/gochecknoglobals v0.2.2/go.mod h1:lLxwTQjL5eIesRbvnzIP3jZtG140FnTdz+AlMa+ogt0=
+cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4=
+cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4=
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=
@@ -17,10 +19,14 @@ cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKV
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.116.0 h1:B3fRrSDkLRt5qSHWe40ERJvhvnQwdZiHu0bJOpldweE=
-cloud.google.com/go v0.116.0/go.mod h1:cEPSRWPzZEswwdr9BxE6ChEn01dWlTaF05LiC2Xs70U=
-cloud.google.com/go/auth v0.15.0 h1:Ly0u4aA5vG/fsSsxu98qCQBemXtAtJf+95z9HK+cxps=
-cloud.google.com/go/auth v0.15.0/go.mod h1:WJDGqZ1o9E9wKIL+IwStfyn/+s59zl4Bi+1KQNVXLZ8=
+cloud.google.com/go v0.120.1 h1:Z+5V7yd383+9617XDCyszmK5E4wJRJL+tquMfDj9hLM=
+cloud.google.com/go v0.120.1/go.mod h1:56Vs7sf/i2jYM6ZL9NYlC82r04PThNcPS5YgFmb0rp8=
+cloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE=
+cloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU=
+cloud.google.com/go/auth v0.16.1 h1:XrXauHMd30LhQYVRHLGvJiYeczweKQXZxsTbV9TiguU=
+cloud.google.com/go/auth v0.16.1/go.mod h1:1howDHJ5IETh/LwYs3ZxvlkXF48aSqqJUM+5o02dNOI=
+cloud.google.com/go/auth v0.18.1 h1:IwTEx92GFUo2pJ6Qea0EU3zYvKnTAeRCODxfA/G5UWs=
+cloud.google.com/go/auth v0.18.1/go.mod h1:GfTYoS9G3CWpRA3Va9doKN9mjPGRS+v41jmZAhBzbrA=
cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
@@ -30,16 +36,29 @@ cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUM
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/compute/metadata v0.2.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k=
-cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I=
-cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg=
+cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
+cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
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.16.0 h1:YwmDHcyrxVRErWcgxunzEaZxtNbc8QoFYA/JOEwDPgc=
-cloud.google.com/go/firestore v1.16.0/go.mod h1:+22v/7p+WNBSQwdSwP57vz47aZiY+HrDkrOsJNhk7rg=
-cloud.google.com/go/iam v1.2.0 h1:kZKMKVNk/IsSSc/udOb83K0hL/Yh/Gcqpz+oAkoIFN8=
-cloud.google.com/go/iam v1.2.0/go.mod h1:zITGuWgsLZxd8OwAlX+eMFgZDXzBm7icj1PVTYG766Q=
-cloud.google.com/go/longrunning v0.6.0 h1:mM1ZmaNsQsnb+5n1DNPeL0KwQd9jQRqSqSDEkBZr+aI=
-cloud.google.com/go/longrunning v0.6.0/go.mod h1:uHzSZqW89h7/pasCWNYdUpwGz3PcVWhrWupreVPYLts=
+cloud.google.com/go/firestore v1.18.0 h1:cuydCaLS7Vl2SatAeivXyhbhDEIR8BDmtn4egDhIn2s=
+cloud.google.com/go/firestore v1.18.0/go.mod h1:5ye0v48PhseZBdcl0qbl3uttu7FIEwEYVaWm0UIEOEU=
+cloud.google.com/go/firestore v1.21.0 h1:BhopUsx7kh6NFx77ccRsHhrtkbJUmDAxNY3uapWdjcM=
+cloud.google.com/go/firestore v1.21.0/go.mod h1:1xH6HNcnkf/gGyR8udd6pFO4Z7GWJSwLKQMx/u6UrP4=
+cloud.google.com/go/iam v1.5.1 h1:hChdlb+noMae8mXHalixvwMEX2LHmmYetIP/efgRQVo=
+cloud.google.com/go/iam v1.5.1/go.mod h1:3oSIQz4eS5EAG0EUlkCbawxv/+2Gc4CruBOQ0igceLE=
+cloud.google.com/go/iam v1.5.3 h1:+vMINPiDF2ognBJ97ABAYYwRgsaqxPbQDlMnbHMjolc=
+cloud.google.com/go/iam v1.5.3/go.mod h1:MR3v9oLkZCTlaqljW6Eb2d3HGDGK5/bDv93jhfISFvU=
+cloud.google.com/go/logging v1.13.0 h1:7j0HgAp0B94o1YRDqiqm26w4q1rDMH7XNRU34lJXHYc=
+cloud.google.com/go/logging v1.13.1 h1:O7LvmO0kGLaHY/gq8cV7T0dyp6zJhYAOtZPX4TF3QtY=
+cloud.google.com/go/logging v1.13.1/go.mod h1:XAQkfkMBxQRjQek96WLPNze7vsOmay9H5PqfsNYDqvw=
+cloud.google.com/go/longrunning v0.6.6 h1:XJNDo5MUfMM05xK3ewpbSdmt7R2Zw+aQEMbdQR65Rbw=
+cloud.google.com/go/longrunning v0.6.6/go.mod h1:hyeGJUrPHcx0u2Uu1UFSoYZLn4lkMrccJig0t4FI7yw=
+cloud.google.com/go/longrunning v0.8.0 h1:LiKK77J3bx5gDLi4SMViHixjD2ohlkwBi+mKA7EhfW8=
+cloud.google.com/go/longrunning v0.8.0/go.mod h1:UmErU2Onzi+fKDg2gR7dusz11Pe26aknR4kHmJJqIfk=
+cloud.google.com/go/monitoring v1.24.1 h1:vKiypZVFD/5a3BbQMvI4gZdl8445ITzXFh257XBgrS0=
+cloud.google.com/go/monitoring v1.24.1/go.mod h1:Z05d1/vn9NaujqY2voG6pVQXoJGbp+r3laV+LySt9K0=
+cloud.google.com/go/monitoring v1.24.3 h1:dde+gMNc0UhPZD1Azu6at2e79bfdztVDS5lvhOdsgaE=
+cloud.google.com/go/monitoring v1.24.3/go.mod h1:nYP6W0tm3N9H/bOw8am7t62YTzZY+zUeQ+Bi6+2eonI=
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=
@@ -49,8 +68,13 @@ cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0Zeo
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=
-cloud.google.com/go/storage v1.43.0 h1:CcxnSohZwizt4LCzQHWvBf1/kvtHUn7gk9QERXPyXFs=
-cloud.google.com/go/storage v1.43.0/go.mod h1:ajvxEa7WmZS1PxvKRq4bq0tFT3vMd502JwstCcYv0Q0=
+cloud.google.com/go/storage v1.51.0 h1:ZVZ11zCiD7b3k+cH5lQs/qcNaoSz3U9I0jgwVzqDlCw=
+cloud.google.com/go/storage v1.51.0/go.mod h1:YEJfu/Ki3i5oHC/7jyTgsGZwdQ8P9hqMqvpi5kRKGgc=
+cloud.google.com/go/storage v1.60.0 h1:oBfZrSOCimggVNz9Y/bXY35uUcts7OViubeddTTVzQ8=
+cloud.google.com/go/storage v1.60.0/go.mod h1:q+5196hXfejkctrnx+VYU8RKQr/L3c0cBIlrjmiAKE0=
+cloud.google.com/go/trace v1.11.3 h1:c+I4YFjxRQjvAhRmSsmjpASUKq88chOX854ied0K/pE=
+cloud.google.com/go/trace v1.11.7 h1:kDNDX8JkaAG3R2nq1lIdkb7FCSi1rCmsEtKVsty7p+U=
+cloud.google.com/go/trace v1.11.7/go.mod h1:TNn9d5V3fQVf6s4SCveVMIBS2LJUqo73GACmq/Tky0s=
codeberg.org/go-fonts/dejavu v0.4.0 h1:2yn58Vkh4CFK3ipacWUAIE3XVBGNa0y1bc95Bmfx91I=
codeberg.org/go-fonts/dejavu v0.4.0/go.mod h1:abni088lmhQJvso2Lsb7azCKzwkfcnttl6tL1UTWKzg=
codeberg.org/go-fonts/liberation v0.5.0 h1:SsKoMO1v1OZmzkG2DY+7ZkCL9U+rrWI09niOLfQ5Bo0=
@@ -78,6 +102,8 @@ github.com/99designs/gqlgen v0.17.64 h1:BzpqO5ofQXyy2XOa93Q6fP1BHLRjTOeU35ovTEsb
github.com/99designs/gqlgen v0.17.64/go.mod h1:kaxLetFxPGeBBwiuKk75NxuI1fe9HRvob17In74v/Zc=
github.com/Abirdcfly/dupword v0.1.3 h1:9Pa1NuAsZvpFPi9Pqkd93I7LIYRURj+A//dFd5tgBeE=
github.com/Abirdcfly/dupword v0.1.3/go.mod h1:8VbB2t7e10KRNdwTVoxdBaxla6avbhGzb8sCTygUMhw=
+github.com/Acconut/go-httptest-recorder v1.0.0 h1:TAv2dfnqp/l+SUvIaMAUK4GeN4+wqb6KZsFFFTGhoJg=
+github.com/Acconut/go-httptest-recorder v1.0.0/go.mod h1:CwQyhTH1kq/gLyWiRieo7c0uokpu3PXeyF/nZjUNtmM=
github.com/Antonboom/errname v1.1.0 h1:A+ucvdpMwlo/myWrkHEUEBWc/xuXdud23S8tmTb/oAE=
github.com/Antonboom/errname v1.1.0/go.mod h1:O1NMrzgUcVBGIfi3xlVuvX8Q/VP/73sseCaAppfjqZw=
github.com/Antonboom/nilnil v1.1.0 h1:jGxJxjgYS3VUUtOTNk8Z1icwT5ESpLH/426fjmQG+ng=
@@ -106,6 +132,17 @@ github.com/Djarvur/go-err113 v0.0.0-20210108212216-aea10b59be24 h1:sHglBQTwgx+rW
github.com/Djarvur/go-err113 v0.0.0-20210108212216-aea10b59be24/go.mod h1:4UJr5HIiMZrwgkSPdsjy2uOQExX/WEILpIrO9UPGuXs=
github.com/GaijinEntertainment/go-exhaustruct/v3 v3.3.1 h1:Sz1JIXEcSfhz7fUi7xHnhpIE0thVASYjvosApmHuD2k=
github.com/GaijinEntertainment/go-exhaustruct/v3 v3.3.1/go.mod h1:n/LSCXNuIYqVfBlVXyHfMQkZDdp1/mmxfSjADd3z1Zg=
+github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0 h1:sBEjpZlNHzK1voKq9695PJSX2o5NEXl7/OL3coiIY0c=
+github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0/go.mod h1:P4WPRUkOhJC13W//jWpyfJNDAIpvRbAUIYLX/4jtlE0=
+github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.51.0 h1:fYE9p3esPxA/C0rQ0AHhP0drtPXDRhaWiwg1DPqO7IU=
+github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.51.0/go.mod h1:BnBReJLvVYx2CS/UHOgVz2BXKXD9wsQPxZug20nZhd0=
+github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0 h1:UnDZ/zFfG1JhH/DqxIZYU/1CUAlTUScoXD/LcM2Ykk8=
+github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0/go.mod h1:IA1C1U7jO/ENqm/vhi7V9YYpBsp+IMyqNrEN94N7tVc=
+github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.51.0 h1:OqVGm6Ei3x5+yZmSJG1Mh2NwHvpVmZ08CB5qJhT9Nuk=
+github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.55.0 h1:7t/qx5Ost0s0wbA/VDrByOooURhp+ikYwv20i9Y07TQ=
+github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.55.0/go.mod h1:vB2GH9GAYYJTO3mEn8oYwzEdhlayZIdQz6zdzgUIRvA=
+github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0 h1:0s6TxfCu2KHkkZPnBfsQ2y5qia0jl3MMrmBhu3nCOYk=
+github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0/go.mod h1:Mf6O40IAyB9zR/1J8nGDDPirZQQPbYJni8Yisy7NTMc=
github.com/Kagami/go-avif v0.1.0 h1:8GHAGLxCdFfhpd4Zg8j1EqO7rtcQNenxIDerC/uu68w=
github.com/Kagami/go-avif v0.1.0/go.mod h1:OPmPqzNdQq3+sXm0HqaUJQ9W/4k+Elbc3RSfJUemDKA=
github.com/Masterminds/semver/v3 v3.3.1 h1:QtNSWtVZ3nBfk8mAOu/B6v7FMJ+NHTIgUPi7rj+4nv4=
@@ -301,6 +338,8 @@ github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUK
github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU=
github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
+github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 h1:6xNmx7iTtyBRev0+D/Tv1FZd4SCg8axKApyNyRsAt/w=
+github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5/go.mod h1:KdCmV+x/BuvyMxRnYBlmVaq4OLiKW6iRQfvC62cvdkI=
github.com/cockroachdb/errors v1.11.3 h1:5bA+k2Y6r+oz/6Z/RFlNeVCesGARKuC6YymtcDrbC/I=
github.com/cockroachdb/errors v1.11.3/go.mod h1:m4UIW4CDjx+R5cybPsNrRbreomiFqt8o1h1wUVazSd8=
github.com/cockroachdb/fifo v0.0.0-20240606204812-0bbfbd93a7ce h1:giXvy4KSc/6g/esnpM7Geqxka4WSqI1SZc7sMJFd3y4=
@@ -395,7 +434,15 @@ github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FM
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.14.0 h1:hbG2kr4RuFj222B6+7T83thSPqLjwBIfQawTkC++2HA=
+github.com/envoyproxy/go-control-plane v0.14.0/go.mod h1:NcS5X47pLl/hfqxU70yPwL9ZMkUlwlKxtAohpi2wBEU=
+github.com/envoyproxy/go-control-plane/envoy v1.36.0 h1:yg/JjO5E7ubRyKX3m07GF3reDNEnfOboJ0QySbH736g=
+github.com/envoyproxy/go-control-plane/envoy v1.36.0/go.mod h1:ty89S1YCCVruQAm9OtKeEkQLTb+Lkz0k8v9W0Oxsv98=
+github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI=
+github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
+github.com/envoyproxy/protoc-gen-validate v1.3.0 h1:TvGH1wof4H33rezVKWSpqKz5NXWg5VPuZ0uONDT6eb4=
+github.com/envoyproxy/protoc-gen-validate v1.3.0/go.mod h1:HvYl7zwPa5mffgyeTUHA9zHIH36nmrm7oCbo4YKoSWA=
github.com/ethereum/c-kzg-4844/v2 v2.1.1 h1:KhzBVjmURsfr1+S3k/VE35T02+AW2qU9t9gr4R6YpSo=
github.com/ethereum/c-kzg-4844/v2 v2.1.1/go.mod h1:TC48kOKjJKPbN7C++qIgt0TJzZ70QznYR7Ob+WXl57E=
github.com/ethereum/go-ethereum v1.16.2 h1:VDHqj86DaQiMpnMgc7l0rwZTg0FRmlz74yupSG5SnzI=
@@ -461,6 +508,8 @@ github.com/go-gst/go-glib v1.4.0 h1:FB2uVfB0uqz7/M6EaDdWWlBZRQpvFAbWfL7drdw8lAE=
github.com/go-gst/go-glib v1.4.0/go.mod h1:GUIpWmkxQ1/eL+FYSjKpLDyTZx6Vgd9nNXt8dA31d5M=
github.com/go-gst/go-gst v1.4.0 h1:EikB43u4c3wc8d2RzlFRSfIGIXYzDy6Zls2vJqrG2BU=
github.com/go-gst/go-gst v1.4.0/go.mod h1:p8TLGtOxJLcrp6PCkTPdnanwWBxPZvYiHDbuSuwgO3c=
+github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
+github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
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=
@@ -538,8 +587,8 @@ github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69
github.com/golang-jwt/jwt/v4 v4.4.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=
github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
-github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
-github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
+github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
+github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA=
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A=
@@ -547,8 +596,8 @@ github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EO
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
-github.com/golang/glog v1.2.4 h1:CNNw5U8lSiiBk7druxtSHHTsRWcxKoac6kZKm2peBBc=
-github.com/golang/glog v1.2.4/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w=
+github.com/golang/glog v1.2.5 h1:DrW6hGnjIhtvhOIiAKT6Psh/Kd/ldepEa81DKeiRJ5I=
+github.com/golang/glog v1.2.5/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w=
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=
@@ -647,12 +696,14 @@ github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
-github.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4=
-github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA=
+github.com/googleapis/enterprise-certificate-proxy v0.3.11 h1:vAe81Msw+8tKUxi2Dqh/NZMz7475yUvmRIkXr4oN2ao=
+github.com/googleapis/enterprise-certificate-proxy v0.3.11/go.mod h1:RFV7MUdlb7AgEq2v7FmMCfeSMCllAzWxFgRdusoGks8=
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/googleapis/gax-go/v2 v2.14.1 h1:hb0FFeiPaQskmvakKu5EbCbpntQn48jyHuvrkurSS/Q=
github.com/googleapis/gax-go/v2 v2.14.1/go.mod h1:Hb/NubMaVM88SrNkvl8X/o8XWwDJEPqouaLeN2IUxoA=
+github.com/googleapis/gax-go/v2 v2.17.0 h1:RksgfBpxqff0EZkDWYuz9q/uWsTVz+kf43LsZ1J6SMc=
+github.com/googleapis/gax-go/v2 v2.17.0/go.mod h1:mzaqghpQp4JDh3HvADwrat+6M3MOIDp5YKHhb9PAgDY=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gopherjs/gopherjs v0.0.0-20190430165422-3e4dfb77656c h1:7lF+Vz0LqiRidnzC1Oq86fpX1q/iEv2KJdrCtttYjT4=
github.com/gopherjs/gopherjs v0.0.0-20190430165422-3e4dfb77656c/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
@@ -677,8 +728,8 @@ github.com/gostaticanalysis/testutil v0.5.0 h1:Dq4wT1DdTwTGCQQv3rl3IvD5Ld0E6HiY+
github.com/gostaticanalysis/testutil v0.5.0/go.mod h1:OLQSbuM6zw2EvCcXTz1lVq5unyoNft372msDY0nY5Hs=
github.com/graph-gophers/graphql-go v1.3.0 h1:Eb9x/q6MFpCLz7jBCiP/WTxjSDrYLR1QY41SORZyNJ0=
github.com/graph-gophers/graphql-go v1.3.0/go.mod h1:9CQHMSxwO4MprSdzoIEobiHpoLtHm77vfxsvsIN5Vuc=
-github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1 h1:e9Rjr40Z98/clHv5Yg79Is0NtosR5LXRvdr7o/6NwbA=
-github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1/go.mod h1:tIxuGz/9mpox++sgp9fJjHO0+q1X9/UOWd798aAm22M=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 h1:X5VWvz21y3gzm9Nw/kaUeku/1+uBhcekkmy4IkffJww=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1/go.mod h1:Zanoh4+gvIgluNqcfMVTJueD4wSS5hT7zTt4Mrutd90=
github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed h1:5upAirOpQc1Q53c0bnx2ufif5kANL7bfZWcc6VJWJd8=
github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed/go.mod h1:tMWxXQ9wFIaZeTI9F+hmhFiGpFmhOHzyShyFUhRm0H4=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
@@ -1160,6 +1211,8 @@ github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/profile v1.4.0/go.mod h1:NWz/XGvpEW1FyYQ7fCx4dqYBLlfTcE+A9FLAkNKqjFE=
+github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo=
+github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
@@ -1181,8 +1234,8 @@ github.com/prometheus/client_golang v1.13.0/go.mod h1:vTeo+zgvILHsnnj/39Ou/1fPN5
github.com/prometheus/client_golang v1.14.0/go.mod h1:8vpkKitgIVNcqrRBWh1C4TIUQgYNtG/XQE4E/Zae36Y=
github.com/prometheus/client_golang v1.15.1/go.mod h1:e9yaBhRPU2pPNsZwE+JdQl0KEt1N9XgF6zxWmaC0xOk=
github.com/prometheus/client_golang v1.17.0/go.mod h1:VeL+gMmOAxkS2IqfCq0ZmHSL+LjWfWDUmp1mBz9JgUY=
-github.com/prometheus/client_golang v1.23.0 h1:ust4zpdl9r4trLY/gSjlm07PuiBq2ynaXXlptpfy8Uc=
-github.com/prometheus/client_golang v1.23.0/go.mod h1:i/o0R9ByOnHX0McrTMTyhYvKE4haaf2mW08I+jGAjEE=
+github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
+github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
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=
@@ -1200,8 +1253,8 @@ github.com/prometheus/common v0.35.0/go.mod h1:phzohg0JFMnBEFGxTDbfu3QyL5GI8gTQJ
github.com/prometheus/common v0.37.0/go.mod h1:phzohg0JFMnBEFGxTDbfu3QyL5GI8gTQJFhYO5B3mfA=
github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc=
github.com/prometheus/common v0.44.0/go.mod h1:ofAIvZbQ1e/nugmZGz4/qCb9Ap1VoSTIO7x0VV9VvuY=
-github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE=
-github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
+github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs=
+github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
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=
@@ -1247,8 +1300,8 @@ github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0t
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA=
github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
-github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc=
-github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
+github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
+github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
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=
@@ -1320,6 +1373,8 @@ github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.12.0 h1:CZ7eSOd3kZoaYDLbXnmzgQI5RlciuXBMA+18HwHRfZQ=
github.com/spf13/viper v1.12.0/go.mod h1:b6COn30jlNxbm/V2IqWiNWkJ+vZNiMNksliPCiuKtSI=
+github.com/spiffe/go-spiffe/v2 v2.6.0 h1:l+DolpxNWYgruGQVV0xsfeya3CsC7m8iBzDnMpsbLuo=
+github.com/spiffe/go-spiffe/v2 v2.6.0/go.mod h1:gm2SeUoMZEtpnzPNs2Csc0D/gX33k1xIx7lEzqblHEs=
github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0=
github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c h1:km8GpoQut05eY3GiYWEedbTT0qnSxrCjsVbb7yKY1KE=
github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c/go.mod h1:cNQ3dwVJtS5Hmnjxy6AgTPd0Inb3pW05ftPSX7NZO7Q=
@@ -1404,6 +1459,10 @@ github.com/tomarrell/wrapcheck/v2 v2.11.0 h1:BJSt36snX9+4WTIXeJ7nvHBQBcm1h2SjQMS
github.com/tomarrell/wrapcheck/v2 v2.11.0/go.mod h1:wFL9pDWDAbXhhPZZt+nG8Fu+h29TtnZ2MW6Lx4BRXIU=
github.com/tommy-muehle/go-mnd/v2 v2.5.1 h1:NowYhSdyE/1zwK9QCLeRb6USWdoif80Ie+v+yU8u1Zw=
github.com/tommy-muehle/go-mnd/v2 v2.5.1/go.mod h1:WsUAkMJMYww6l/ufffCD3m+P7LEvr8TnZn9lwVDlgzw=
+github.com/tus/tusd/v2 v2.8.0 h1:X2jGxQ05jAW4inDd2ogmOKqwnb4c/D0lw2yhgHayWyU=
+github.com/tus/tusd/v2 v2.8.0/go.mod h1:3/zEOVQQIwmJhvNam8phV4x/UQt68ZmZiTzeuJUNhVo=
+github.com/tus/tusd/v2 v2.9.2 h1:Dd/Dh0CG7+/wom4lDQnnhca+1p5qVwgnbyEBacj1v7c=
+github.com/tus/tusd/v2 v2.9.2/go.mod h1:+a9uNLru2Qy+CUu7QUIshmQ+X0fLNw77eu8voKVxmgA=
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/ultraware/funlen v0.2.0 h1:gCHmCn+d2/1SemTdYMiKLAHFYxTYz7z9VIDRaTGyLkI=
@@ -1500,28 +1559,33 @@ go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
-go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
-go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
-go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0 h1:rgMkmiGfix9vFJDcDi1PK8WEQP4FLQwLDfhp5ZLpFeE=
-go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0/go.mod h1:ijPqXp5P6IRRByFVVg9DY8P5HkxkHE5ARIa+86aXPf4=
+go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
+go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
+go.opentelemetry.io/contrib/detectors/gcp v1.39.0 h1:kWRNZMsfBHZ+uHjiH4y7Etn2FK26LAGkNFw7RHv1DhE=
+go.opentelemetry.io/contrib/detectors/gcp v1.39.0/go.mod h1:t/OGqzHBa5v6RHZwrDBJ2OirWc+4q/w2fTbLZwAKjTk=
+go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 h1:YH4g8lQroajqUwWbq/tr2QX1JFmEXaDLgG+ew9bLMWo=
+go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0/go.mod h1:fvPi2qXDqFs8M4B4fmJhE92TyQs9Ydjlg3RvfUp+NbQ=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q=
-go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg=
-go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E=
+go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
+go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 h1:1fTNlAIJZGWLP5FVu0fikVry1IsiUnXjf7QFvoNN3Xw=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0/go.mod h1:zjPK58DtkqQFn+YUMbx0M2XV3QgKU0gS9LeGohREyK4=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.33.0 h1:wpMfgF8E1rkrT1Z6meFh1NDtownE9Ii3n3X2GJYjsaU=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.33.0/go.mod h1:wAy0T/dUbs468uOlkT31xjvqQgEVXv58BRFWEgn5v/0=
-go.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCREuTYufE=
-go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs=
-go.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs=
-go.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY=
-go.opentelemetry.io/otel/sdk/metric v1.36.0 h1:r0ntwwGosWGaa0CrSt8cuNuTcccMXERFwHX4dThiPis=
-go.opentelemetry.io/otel/sdk/metric v1.36.0/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4=
-go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w=
-go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA=
-go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4=
-go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4=
+go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.29.0 h1:WDdP9acbMYjbKIyJUhTvtzj601sVJOqgWdUxSdR/Ysc=
+go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.39.0 h1:5gn2urDL/FBnK8OkCfD1j3/ER79rUuTYmCvlXBKeYL8=
+go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.39.0/go.mod h1:0fBG6ZJxhqByfFZDwSwpZGzJU671HkwpWaNe2t4VUPI=
+go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
+go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
+go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
+go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
+go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=
+go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
+go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
+go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
+go.opentelemetry.io/proto/otlp v1.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOVAtj4=
+go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE=
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.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
@@ -1541,6 +1605,8 @@ go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9E
go.uber.org/zap v1.16.0/go.mod h1:MA8QOfq0BHJwdXa996Y4dYkAqRKB8/1K1QMMZVaNZjQ=
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
+go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
+go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/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=
@@ -1673,8 +1739,8 @@ golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ
golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
golang.org/x/oauth2 v0.5.0/go.mod h1:9/XBHVqLaWO3/BRHs5jbpYCnOZVjj5V0ndyaAM7KB4I=
golang.org/x/oauth2 v0.8.0/go.mod h1:yr7u4HXZRm1R1kBWqr/xKNqewf0plRYoB7sla+BCIXE=
-golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
-golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
+golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ=
+golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
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=
@@ -1796,8 +1862,8 @@ golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
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/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
-golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
+golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
+golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
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=
@@ -1872,6 +1938,8 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY=
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
+gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
+gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
gonum.org/v1/plot v0.16.0 h1:dK28Qx/Ky4VmPUN/2zeW0ELyM6ucDnBAj5yun7M9n1g=
gonum.org/v1/plot v0.16.0/go.mod h1:Xz6U1yDMi6Ni6aaXILqmVIb6Vro8E+K7Q/GeeH+Pn0c=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
@@ -1892,6 +1960,8 @@ google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSr
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
google.golang.org/api v0.228.0 h1:X2DJ/uoWGnY5obVjewbp8icSL5U4FzuCfy9OjbLSnLs=
google.golang.org/api v0.228.0/go.mod h1:wNvRS1Pbe8r4+IfBIniV8fwCpGwTrYa+kMUDiC5z5a4=
+google.golang.org/api v0.265.0 h1:FZvfUdI8nfmuNrE34aOWFPmLC+qRBEiNm3JdivTvAAU=
+google.golang.org/api v0.265.0/go.mod h1:uAvfEl3SLUj/7n6k+lJutcswVojHPp2Sp08jWCu8hLY=
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=
@@ -1930,12 +2000,14 @@ google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7Fc
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-20240903143218-8af14fe29dc1 h1:BulPr26Jqjnd4eYDVe+YvyR7Yc2vJGkO5/0UxD0/jZU=
-google.golang.org/genproto v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:hL97c3SYopEHblzpxRL4lSs523++l8DYxGM1FQiYmb4=
-google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a h1:nwKuGPlUAt+aR+pcrkfFRrTU1BVrSmYyYMxYbUIVHr0=
-google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a/go.mod h1:3kWAYMk1I75K4vykHtKt2ycnOgpA6974V7bREqbsenU=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4 h1:iK2jbkWL86DXjEx0qiHcRE9dE4/Ahua5k6V8OWFb//c=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I=
+google.golang.org/genproto v0.0.0-20250303144028-a0af3efb3deb h1:ITgPrl429bc6+2ZraNSzMDk3I95nmQln2fuPstKwFDE=
+google.golang.org/genproto v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:sAo5UzpjUwgFBCzupwhcLcxHVDK7vG5IqI30YnwX2eE=
+google.golang.org/genproto v0.0.0-20260128011058-8636f8732409 h1:VQZ/yAbAtjkHgH80teYd2em3xtIkkHd7ZhqfH2N9CsM=
+google.golang.org/genproto v0.0.0-20260128011058-8636f8732409/go.mod h1:rxKD3IEILWEu3P44seeNOAwZN4SaoKaQ/2eTg4mM6EM=
+google.golang.org/genproto/googleapis/api v0.0.0-20260203192932-546029d2fa20 h1:7ei4lp52gK1uSejlA8AZl5AJjeLUOHBQscRQZUgAcu0=
+google.golang.org/genproto/googleapis/api v0.0.0-20260203192932-546029d2fa20/go.mod h1:ZdbssH/1SOVnjnDlXzxDHK2MCidiqXtbYccJNzNYPEE=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20 h1:Jr5R2J6F6qWyzINc+4AM8t5pfUz6beZpHp678GNrMbE=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
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=
@@ -1949,8 +2021,8 @@ google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3Iji
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.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
-google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg=
-google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec=
+google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY=
+google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
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=
@@ -1968,8 +2040,8 @@ google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqw
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
-google.golang.org/protobuf v1.36.7 h1:IgrO7UwFQGJdRNXH/sQux4R1Dj1WAKcLElzeeRaXV2A=
-google.golang.org/protobuf v1.36.7/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
+google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
+google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
diff --git a/js/docs/src/content/docs/lex-reference/media/place-stream-media-createupload.md b/js/docs/src/content/docs/lex-reference/media/place-stream-media-createupload.md
new file mode 100644
index 00000000..1255526e
--- /dev/null
+++ b/js/docs/src/content/docs/lex-reference/media/place-stream-media-createupload.md
@@ -0,0 +1,125 @@
+---
+title: place.stream.media.createUpload
+description: Reference for the place.stream.media.createUpload lexicon
+---
+
+**Lexicon Version:** 1
+
+## Definitions
+
+
+
+### `main`
+
+**Type:** `procedure`
+
+Start a resumable upload of arbitrary media content. Returns a TUS upload URL and a short-lived bearer token used to authenticate subsequent chunk requests (no DPoP required on chunks). The DID of the authenticated user is recorded against the upload; on completion a VOD processing task is enqueued.
+
+**Parameters:** _(None defined)_
+
+**Input:**
+
+- **Encoding:** `application/json`
+- **Schema:**
+
+**Schema Type:** `object`
+
+| Name | Type | Req'd | Description | Constraints |
+| ---------- | --------- | ----- | --------------------------------------------------------- | ----------- |
+| `size` | `integer` | ✅ | Total size of the upload in bytes. | |
+| `mimeType` | `string` | ✅ | MIME type of the content being uploaded (e.g. video/mp4). | |
+| `filename` | `string` | ❌ | Optional filename hint to attach as upload metadata. | |
+
+**Output:**
+
+- **Encoding:** `application/json`
+- **Schema:**
+
+**Schema Type:** `object`
+
+| Name | Type | Req'd | Description | Constraints |
+| ------------- | -------- | ----- | ------------------------------------------------------------------------------------------------------- | ------------------ |
+| `uploadId` | `string` | ✅ | Server-side identifier for this upload. | |
+| `uploadUrl` | `string` | ✅ | Absolute URL to PATCH/HEAD per the TUS protocol. | Format: `uri` |
+| `uploadToken` | `string` | ✅ | Short-lived bearer token bound to this upload. Send as 'Authorization: Bearer ' on TUS requests. | |
+| `expiresAt` | `string` | ✅ | When the upload token expires. | Format: `datetime` |
+
+**Possible Errors:**
+
+- `UploadTooLarge`: The requested upload exceeds the server's maximum allowed size.
+- `UnsupportedMimeType`: The server does not accept this MIME type for upload.
+
+---
+
+## Lexicon Source
+
+```json
+{
+ "lexicon": 1,
+ "id": "place.stream.media.createUpload",
+ "defs": {
+ "main": {
+ "type": "procedure",
+ "description": "Start a resumable upload of arbitrary media content. Returns a TUS upload URL and a short-lived bearer token used to authenticate subsequent chunk requests (no DPoP required on chunks). The DID of the authenticated user is recorded against the upload; on completion a VOD processing task is enqueued.",
+ "input": {
+ "encoding": "application/json",
+ "schema": {
+ "type": "object",
+ "required": ["size", "mimeType"],
+ "properties": {
+ "size": {
+ "type": "integer",
+ "description": "Total size of the upload in bytes."
+ },
+ "mimeType": {
+ "type": "string",
+ "description": "MIME type of the content being uploaded (e.g. video/mp4)."
+ },
+ "filename": {
+ "type": "string",
+ "description": "Optional filename hint to attach as upload metadata."
+ }
+ }
+ }
+ },
+ "output": {
+ "encoding": "application/json",
+ "schema": {
+ "type": "object",
+ "required": ["uploadUrl", "uploadToken", "expiresAt", "uploadId"],
+ "properties": {
+ "uploadId": {
+ "type": "string",
+ "description": "Server-side identifier for this upload."
+ },
+ "uploadUrl": {
+ "type": "string",
+ "format": "uri",
+ "description": "Absolute URL to PATCH/HEAD per the TUS protocol."
+ },
+ "uploadToken": {
+ "type": "string",
+ "description": "Short-lived bearer token bound to this upload. Send as 'Authorization: Bearer ' on TUS requests."
+ },
+ "expiresAt": {
+ "type": "string",
+ "format": "datetime",
+ "description": "When the upload token expires."
+ }
+ }
+ }
+ },
+ "errors": [
+ {
+ "name": "UploadTooLarge",
+ "description": "The requested upload exceeds the server's maximum allowed size."
+ },
+ {
+ "name": "UnsupportedMimeType",
+ "description": "The server does not accept this MIME type for upload."
+ }
+ ]
+ }
+ }
+}
+```
diff --git a/js/docs/src/content/docs/lex-reference/openapi.json b/js/docs/src/content/docs/lex-reference/openapi.json
index 55df0de3..75634ae3 100644
--- a/js/docs/src/content/docs/lex-reference/openapi.json
+++ b/js/docs/src/content/docs/lex-reference/openapi.json
@@ -1546,6 +1546,103 @@
}
}
},
+ "/xrpc/place.stream.media.createUpload": {
+ "post": {
+ "summary": "Start a resumable upload of arbitrary media content. Returns a TUS upload URL and a short-lived bearer token used to authenticate subsequent chunk requests (no DPoP required on chunks). The DID of the authenticated user is recorded against the upload; on completion a VOD processing task is enqueued.",
+ "operationId": "place.stream.media.createUpload",
+ "tags": ["place.stream.media"],
+ "responses": {
+ "200": {
+ "description": "Success",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "uploadId": {
+ "type": "string",
+ "description": "Server-side identifier for this upload."
+ },
+ "uploadUrl": {
+ "type": "string",
+ "description": "Absolute URL to PATCH/HEAD per the TUS protocol.",
+ "format": "uri"
+ },
+ "uploadToken": {
+ "type": "string",
+ "description": "Short-lived bearer token bound to this upload. Send as 'Authorization: Bearer ' on TUS requests."
+ },
+ "expiresAt": {
+ "type": "string",
+ "description": "When the upload token expires.",
+ "format": "date-time"
+ }
+ },
+ "required": [
+ "uploadUrl",
+ "uploadToken",
+ "expiresAt",
+ "uploadId"
+ ]
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Bad Request",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "required": ["error", "message"],
+ "properties": {
+ "error": {
+ "type": "string",
+ "oneOf": [
+ {
+ "const": "UploadTooLarge"
+ },
+ {
+ "const": "UnsupportedMimeType"
+ }
+ ]
+ },
+ "message": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "size": {
+ "type": "integer",
+ "description": "Total size of the upload in bytes."
+ },
+ "mimeType": {
+ "type": "string",
+ "description": "MIME type of the content being uploaded (e.g. video/mp4)."
+ },
+ "filename": {
+ "type": "string",
+ "description": "Optional filename hint to attach as upload metadata."
+ }
+ },
+ "required": ["size", "mimeType"]
+ }
+ }
+ }
+ }
+ }
+ },
"/xrpc/place.stream.live.denyTeleport": {
"post": {
"summary": "Deny an incoming teleport request.",
diff --git a/lexicons/place/stream/media/createUpload.json b/lexicons/place/stream/media/createUpload.json
new file mode 100644
index 00000000..52b44f9a
--- /dev/null
+++ b/lexicons/place/stream/media/createUpload.json
@@ -0,0 +1,68 @@
+{
+ "lexicon": 1,
+ "id": "place.stream.media.createUpload",
+ "defs": {
+ "main": {
+ "type": "procedure",
+ "description": "Start a resumable upload of arbitrary media content. Returns a TUS upload URL and a short-lived bearer token used to authenticate subsequent chunk requests (no DPoP required on chunks). The DID of the authenticated user is recorded against the upload; on completion a VOD processing task is enqueued.",
+ "input": {
+ "encoding": "application/json",
+ "schema": {
+ "type": "object",
+ "required": ["size", "mimeType"],
+ "properties": {
+ "size": {
+ "type": "integer",
+ "description": "Total size of the upload in bytes."
+ },
+ "mimeType": {
+ "type": "string",
+ "description": "MIME type of the content being uploaded (e.g. video/mp4)."
+ },
+ "filename": {
+ "type": "string",
+ "description": "Optional filename hint to attach as upload metadata."
+ }
+ }
+ }
+ },
+ "output": {
+ "encoding": "application/json",
+ "schema": {
+ "type": "object",
+ "required": ["uploadUrl", "uploadToken", "expiresAt", "uploadId"],
+ "properties": {
+ "uploadId": {
+ "type": "string",
+ "description": "Server-side identifier for this upload."
+ },
+ "uploadUrl": {
+ "type": "string",
+ "format": "uri",
+ "description": "Absolute URL to PATCH/HEAD per the TUS protocol."
+ },
+ "uploadToken": {
+ "type": "string",
+ "description": "Short-lived bearer token bound to this upload. Send as 'Authorization: Bearer ' on TUS requests."
+ },
+ "expiresAt": {
+ "type": "string",
+ "format": "datetime",
+ "description": "When the upload token expires."
+ }
+ }
+ }
+ },
+ "errors": [
+ {
+ "name": "UploadTooLarge",
+ "description": "The requested upload exceeds the server's maximum allowed size."
+ },
+ {
+ "name": "UnsupportedMimeType",
+ "description": "The server does not accept this MIME type for upload."
+ }
+ ]
+ }
+ }
+}
diff --git a/pkg/api/api.go b/pkg/api/api.go
index 4e17d631..0a8d0eb2 100644
--- a/pkg/api/api.go
+++ b/pkg/api/api.go
@@ -45,6 +45,7 @@ import (
"stream.place/streamplace/pkg/spxrpc"
"stream.place/streamplace/pkg/statedb"
"stream.place/streamplace/pkg/streamplace"
+ "stream.place/streamplace/pkg/upload"
metrics "github.com/slok/go-http-metrics/metrics/prometheus"
"github.com/slok/go-http-metrics/middleware"
@@ -64,6 +65,7 @@ type StreamplaceAPI struct {
FirebaseNotifier notifications.FirebaseNotifier
MediaManager *media.MediaManager
MediaSigner media.MediaSigner
+ UploadManager *upload.Manager
XRPCServer *spxrpc.Server
// not thread-safe yet
Aliases map[string]string
@@ -95,7 +97,7 @@ type WebsocketTracker struct {
mu sync.RWMutex
}
-func MakeStreamplaceAPI(cli *config.CLI, mod model.Model, statefulDB *statedb.StatefulDB, noter notifications.FirebaseNotifier, mm *media.MediaManager, ms media.MediaSigner, bus *bus.Bus, atsync *atproto.ATProtoSynchronizer, d *director.Director, op *oatproxy.OATProxy, ldb localdb.LocalDB) (*StreamplaceAPI, error) {
+func MakeStreamplaceAPI(cli *config.CLI, mod model.Model, statefulDB *statedb.StatefulDB, noter notifications.FirebaseNotifier, mm *media.MediaManager, ms media.MediaSigner, bus *bus.Bus, atsync *atproto.ATProtoSynchronizer, d *director.Director, op *oatproxy.OATProxy, ldb localdb.LocalDB, um *upload.Manager) (*StreamplaceAPI, error) {
updater, err := PrepareUpdater(cli)
if err != nil {
return nil, err
@@ -107,6 +109,7 @@ func MakeStreamplaceAPI(cli *config.CLI, mod model.Model, statefulDB *statedb.St
FirebaseNotifier: noter,
MediaManager: mm,
MediaSigner: ms,
+ UploadManager: um,
Aliases: map[string]string{},
Bus: bus,
ATSync: atsync,
@@ -155,7 +158,7 @@ func (a *StreamplaceAPI) Handler(ctx context.Context) (http.Handler, error) {
Recorder: metrics.NewRecorder(metrics.Config{}),
})
var xrpc http.Handler
- xrpc, err := spxrpc.NewServer(ctx, a.CLI, a.Model, a.StatefulDB, a.op, mdlw, a.ATSync, a.Bus, a.LocalDB, a.MediaManager, a.Aliases)
+ xrpc, err := spxrpc.NewServer(ctx, a.CLI, a.Model, a.StatefulDB, a.op, mdlw, a.ATSync, a.Bus, a.LocalDB, a.MediaManager, a.UploadManager, a.Aliases)
if err != nil {
return nil, err
}
@@ -209,6 +212,13 @@ func (a *StreamplaceAPI) Handler(ctx context.Context) (http.Handler, error) {
addHandle(apiRouter, "GET", "/api/bluesky/resolve/:handle", a.HandleBlueskyResolve(ctx))
addHandle(apiRouter, "GET", "/api/view-count/:user", a.HandleViewCount(ctx))
addHandle(apiRouter, "GET", "/api/clip/:user/:file", a.HandleClip(ctx))
+ if a.UploadManager != nil {
+ uploadHandler := middlewarestd.Handler("/api/upload/:id", mdlw, a.UploadManager)
+ apiRouter.Handler("HEAD", "/api/upload/:id", uploadHandler)
+ apiRouter.Handler("PATCH", "/api/upload/:id", uploadHandler)
+ apiRouter.Handler("DELETE", "/api/upload/:id", uploadHandler)
+ apiRouter.Handler("OPTIONS", "/api/upload/:id", uploadHandler)
+ }
apiRouter.NotFound = a.HandleAPI404(ctx)
apiRouterHandler := a.RateLimitMiddleware(ctx)(apiRouter)
xrpcHandler := a.RateLimitMiddleware(ctx)(xrpc)
diff --git a/pkg/cmd/streamplace.go b/pkg/cmd/streamplace.go
index d703e955..3daae113 100644
--- a/pkg/cmd/streamplace.go
+++ b/pkg/cmd/streamplace.go
@@ -41,6 +41,7 @@ import (
"stream.place/streamplace/pkg/spmetrics"
"stream.place/streamplace/pkg/statedb"
"stream.place/streamplace/pkg/storage"
+ "stream.place/streamplace/pkg/upload"
_ "github.com/go-gst/go-glib/glib"
_ "github.com/go-gst/go-gst/gst"
@@ -343,7 +344,11 @@ func runMain(ctx context.Context, build *config.BuildFlags, platformJobs []jobFu
}
d := director.NewDirector(mm, mod, cli, b, op, state, replicator, ldb, atsync)
- a, err := api.MakeStreamplaceAPI(cli, mod, state, noter, mm, ms, b, atsync, d, op, ldb)
+ um, err := upload.New(ctx, cli, state)
+ if err != nil {
+ return err
+ }
+ a, err := api.MakeStreamplaceAPI(cli, mod, state, noter, mm, ms, b, atsync, d, op, ldb, um)
if err != nil {
return err
}
diff --git a/pkg/spxrpc/place_stream_media.go b/pkg/spxrpc/place_stream_media.go
new file mode 100644
index 00000000..81b99227
--- /dev/null
+++ b/pkg/spxrpc/place_stream_media.go
@@ -0,0 +1,64 @@
+package spxrpc
+
+import (
+ "context"
+ "net/http"
+
+ "github.com/labstack/echo/v4"
+ "github.com/streamplace/oatproxy/pkg/oatproxy"
+ placestream "stream.place/streamplace/pkg/streamplace"
+)
+
+func (s *Server) handlePlaceStreamMediaCreateUpload(ctx context.Context, body *placestream.MediaCreateUpload_Input) (*placestream.MediaCreateUpload_Output, error) {
+ session, _ := oatproxy.GetOAuthSession(ctx)
+ if session == nil {
+ return nil, echo.NewHTTPError(http.StatusUnauthorized, "oauth session required")
+ }
+ if s.uploadManager == nil {
+ return nil, echo.NewHTTPError(http.StatusServiceUnavailable, "upload manager not configured")
+ }
+
+ filename := ""
+ if body.Filename != nil {
+ filename = *body.Filename
+ }
+
+ baseURL, err := s.requestBaseURL(ctx)
+ if err != nil {
+ return nil, echo.NewHTTPError(http.StatusInternalServerError, err.Error())
+ }
+
+ res, err := s.uploadManager.Create(ctx, session.DID, body.MimeType, filename, body.Size, baseURL)
+ if err != nil {
+ return nil, echo.NewHTTPError(http.StatusInternalServerError, err.Error())
+ }
+
+ return &placestream.MediaCreateUpload_Output{
+ UploadId: res.UploadID,
+ UploadUrl: res.UploadURL,
+ UploadToken: res.UploadToken,
+ ExpiresAt: res.ExpiresAt.UTC().Format("2006-01-02T15:04:05.000Z"),
+ }, nil
+}
+
+// requestBaseURL returns the scheme+host of the inbound HTTP request, used
+// to construct user-facing URLs that the same client can reach.
+func (s *Server) requestBaseURL(ctx context.Context) (string, error) {
+ ec, ok := ctx.Value(echoContextKey).(echo.Context)
+ if !ok {
+ return "", echo.NewHTTPError(http.StatusInternalServerError, "no echo context")
+ }
+ req := ec.Request()
+ scheme := "https"
+ if !s.cli.Secure {
+ scheme = "http"
+ }
+ if fwd := req.Header.Get("X-Forwarded-Proto"); fwd != "" {
+ scheme = fwd
+ }
+ host := req.Host
+ if fwd := req.Header.Get("X-Forwarded-Host"); fwd != "" {
+ host = fwd
+ }
+ return scheme + "://" + host, nil
+}
diff --git a/pkg/spxrpc/spxrpc.go b/pkg/spxrpc/spxrpc.go
index 4061bee5..5b89183a 100644
--- a/pkg/spxrpc/spxrpc.go
+++ b/pkg/spxrpc/spxrpc.go
@@ -23,6 +23,7 @@ import (
"stream.place/streamplace/pkg/media"
"stream.place/streamplace/pkg/model"
"stream.place/streamplace/pkg/statedb"
+ "stream.place/streamplace/pkg/upload"
)
type Server struct {
@@ -38,10 +39,11 @@ type Server struct {
op *oatproxy.OATProxy
localDB localdb.LocalDB
mm *media.MediaManager
+ uploadManager *upload.Manager
aliases map[string]string
}
-func NewServer(ctx context.Context, cli *config.CLI, model model.Model, statefulDB *statedb.StatefulDB, op *oatproxy.OATProxy, mdlw middleware.Middleware, atsync *atproto.ATProtoSynchronizer, bus *bus.Bus, ldb localdb.LocalDB, mm *media.MediaManager, aliases map[string]string) (*Server, error) {
+func NewServer(ctx context.Context, cli *config.CLI, model model.Model, statefulDB *statedb.StatefulDB, op *oatproxy.OATProxy, mdlw middleware.Middleware, atsync *atproto.ATProtoSynchronizer, bus *bus.Bus, ldb localdb.LocalDB, mm *media.MediaManager, um *upload.Manager, aliases map[string]string) (*Server, error) {
e := echo.New()
s := &Server{
e: e,
@@ -56,6 +58,7 @@ func NewServer(ctx context.Context, cli *config.CLI, model model.Model, stateful
op: op,
localDB: ldb,
mm: mm,
+ uploadManager: um,
aliases: aliases,
}
e.Use(s.ErrorHandlingMiddleware())
diff --git a/pkg/spxrpc/stubs.go b/pkg/spxrpc/stubs.go
index 9ff58d73..397c0882 100644
--- a/pkg/spxrpc/stubs.go
+++ b/pkg/spxrpc/stubs.go
@@ -302,6 +302,7 @@ func (s *Server) RegisterHandlersPlaceStream(e *echo.Echo) error {
e.GET("/xrpc/place.stream.live.searchActorsTypeahead", s.HandlePlaceStreamLiveSearchActorsTypeahead)
e.POST("/xrpc/place.stream.live.startLivestream", s.HandlePlaceStreamLiveStartLivestream)
e.POST("/xrpc/place.stream.live.stopLivestream", s.HandlePlaceStreamLiveStopLivestream)
+ e.POST("/xrpc/place.stream.media.createUpload", s.HandlePlaceStreamMediaCreateUpload)
e.POST("/xrpc/place.stream.moderation.createBlock", s.HandlePlaceStreamModerationCreateBlock)
e.POST("/xrpc/place.stream.moderation.createGate", s.HandlePlaceStreamModerationCreateGate)
e.POST("/xrpc/place.stream.moderation.createPin", s.HandlePlaceStreamModerationCreatePin)
@@ -672,6 +673,24 @@ func (s *Server) HandlePlaceStreamLiveStopLivestream(c echo.Context) error {
return c.JSON(200, out)
}
+func (s *Server) HandlePlaceStreamMediaCreateUpload(c echo.Context) error {
+ ctx, span := otel.Tracer("server").Start(c.Request().Context(), "HandlePlaceStreamMediaCreateUpload")
+ defer span.End()
+
+ var body placestream.MediaCreateUpload_Input
+ if err := c.Bind(&body); err != nil {
+ return err
+ }
+ var out *placestream.MediaCreateUpload_Output
+ var handleErr error
+ // func (s *Server) handlePlaceStreamMediaCreateUpload(ctx context.Context,body *placestream.MediaCreateUpload_Input) (*placestream.MediaCreateUpload_Output, error)
+ out, handleErr = s.handlePlaceStreamMediaCreateUpload(ctx, &body)
+ if handleErr != nil {
+ return handleErr
+ }
+ return c.JSON(200, out)
+}
+
func (s *Server) HandlePlaceStreamModerationCreateBlock(c echo.Context) error {
ctx, span := otel.Tracer("server").Start(c.Request().Context(), "HandlePlaceStreamModerationCreateBlock")
defer span.End()
diff --git a/pkg/statedb/queue_processor.go b/pkg/statedb/queue_processor.go
index a21983b0..03d2b975 100644
--- a/pkg/statedb/queue_processor.go
+++ b/pkg/statedb/queue_processor.go
@@ -23,6 +23,7 @@ import (
var TaskNotification = "notification"
var TaskChat = "chat"
var TaskFinalizeLivestream = "finalize_livestream"
+var TaskVODProcess = "vod_process"
type NotificationTask struct {
Livestream *streamplace.Livestream_LivestreamView
@@ -39,6 +40,20 @@ type FinalizeLivestreamTask struct {
LivestreamURI string `json:"livestreamURI"`
}
+// VODProcessTask is enqueued by the upload manager when a resumable user
+// upload completes. The processor probes the file, generates MUXL tracks,
+// and creates the place.stream.video record set. The actual processing is
+// not yet implemented — for now this just acknowledges receipt.
+type VODProcessTask struct {
+ UploadID string `json:"uploadId"`
+ RepoDID string `json:"repoDID"`
+ MimeType string `json:"mimeType"`
+ Filename string `json:"filename,omitempty"`
+ Size int64 `json:"size"`
+ Backend string `json:"backend"`
+ Location string `json:"location"`
+}
+
func (state *StatefulDB) ProcessQueue(ctx context.Context) error {
for {
task, err := state.DequeueTask(ctx, "queue_processor")
@@ -72,11 +87,32 @@ func (state *StatefulDB) processTask(ctx context.Context, task *AppTask) error {
return state.processChatMessageTask(ctx, task)
case TaskFinalizeLivestream:
return state.processFinalizeLivestreamTask(ctx, task)
+ case TaskVODProcess:
+ return state.processVODProcessTask(ctx, task)
default:
return fmt.Errorf("unknown task type: %s", task.Type)
}
}
+// processVODProcessTask is a stub. The full pipeline (probe, transcode,
+// generate place.stream.media.track / .origin / place.stream.video records)
+// lives in a follow-up change; right now we just log so the queue doesn't
+// keep retrying.
+func (state *StatefulDB) processVODProcessTask(ctx context.Context, task *AppTask) error {
+ ctx = log.WithLogValues(ctx, "func", "processVODProcessTask")
+ var t VODProcessTask
+ if err := json.Unmarshal(task.Payload, &t); err != nil {
+ return err
+ }
+ log.Log(ctx, "vod-process task received (stub)",
+ "uploadId", t.UploadID, "repoDID", t.RepoDID,
+ "backend", t.Backend, "size", t.Size, "mimeType", t.MimeType)
+ if err := state.CompleteTask(ctx, task.ID); err != nil {
+ return err
+ }
+ return nil
+}
+
func (state *StatefulDB) processFinalizeLivestreamTask(ctx context.Context, task *AppTask) error {
ctx = log.WithLogValues(ctx, "func", "processFinalizeLivestreamTask")
log.Debug(ctx, "processing finalize livestream task")
diff --git a/pkg/statedb/statedb.go b/pkg/statedb/statedb.go
index 187455cc..8bc8a7b6 100644
--- a/pkg/statedb/statedb.go
+++ b/pkg/statedb/statedb.go
@@ -56,6 +56,7 @@ var StatefulDBModels = []any{
Storage{},
BroadcastOrigin{},
S3Segment{},
+ Upload{},
}
var NoPostgresDatabaseCode = "3D000"
diff --git a/pkg/statedb/upload.go b/pkg/statedb/upload.go
new file mode 100644
index 00000000..52ed2ea3
--- /dev/null
+++ b/pkg/statedb/upload.go
@@ -0,0 +1,86 @@
+package statedb
+
+import (
+ "context"
+ "crypto/rand"
+ "errors"
+ "time"
+
+ "gorm.io/gorm"
+)
+
+// Upload tracks a TUS resumable upload from a Streamplace user, regardless of
+// whether the bytes land on local disk or in S3. Lifecycle: a row is created
+// by the createUpload XRPC, updated with CompletedAt when the TUS handler's
+// OnUploadFinished hook fires, and read by the VOD processing task.
+type Upload struct {
+ // ID is the TUS upload identifier (also embedded in the bearer token).
+ ID string `gorm:"column:id;primarykey"`
+ RepoDID string `gorm:"column:user_did;index;not null"`
+ MimeType string `gorm:"column:mime_type"`
+ Filename string `gorm:"column:filename"`
+ Size int64 `gorm:"column:size"`
+ Backend string `gorm:"column:backend"` // "file" or "s3"
+ Location string `gorm:"column:location"`
+ CompletedAt *time.Time `gorm:"column:completed_at"`
+ CreatedAt time.Time `gorm:"column:created_at"`
+ UpdatedAt time.Time `gorm:"column:updated_at"`
+}
+
+func (Upload) TableName() string {
+ return "uploads"
+}
+
+func (state *StatefulDB) CreateUpload(ctx context.Context, u *Upload) error {
+ return state.DB.WithContext(ctx).Create(u).Error
+}
+
+func (state *StatefulDB) GetUpload(ctx context.Context, id string) (*Upload, error) {
+ var u Upload
+ err := state.DB.WithContext(ctx).Where("id = ?", id).First(&u).Error
+ if err != nil {
+ if errors.Is(err, gorm.ErrRecordNotFound) {
+ return nil, nil
+ }
+ return nil, err
+ }
+ return &u, nil
+}
+
+func (state *StatefulDB) CompleteUpload(ctx context.Context, id string, location string) error {
+ now := time.Now().UTC()
+ return state.DB.WithContext(ctx).Model(&Upload{}).
+ Where("id = ?", id).
+ Updates(map[string]any{
+ "completed_at": &now,
+ "location": location,
+ }).Error
+}
+
+// uploadAuthKeySize is the size in bytes of the HMAC key used to sign upload
+// bearer tokens. 32 bytes is the recommended size for HS256.
+const uploadAuthKeySize = 32
+
+const uploadAuthKeyConfigKey = "upload-auth-key"
+
+// GetOrCreateUploadAuthKey returns the symmetric HMAC key used to sign upload
+// bearer tokens, lazily generating one on first call. The key is persisted in
+// the stateful database so all nodes in a station agree, exactly like the
+// repo-key flow in pkg/atproto/lexicon_repo.go.
+func (state *StatefulDB) GetOrCreateUploadAuthKey() ([]byte, error) {
+ existing, err := state.GetConfig(uploadAuthKeyConfigKey)
+ if err != nil {
+ return nil, err
+ }
+ if existing != nil {
+ return existing.Value, nil
+ }
+ key := make([]byte, uploadAuthKeySize)
+ if _, err := rand.Read(key); err != nil {
+ return nil, err
+ }
+ if err := state.PutConfig(uploadAuthKeyConfigKey, key); err != nil {
+ return nil, err
+ }
+ return key, nil
+}
diff --git a/pkg/streamplace/mediacreateUpload.go b/pkg/streamplace/mediacreateUpload.go
new file mode 100644
index 00000000..7f0015b8
--- /dev/null
+++ b/pkg/streamplace/mediacreateUpload.go
@@ -0,0 +1,43 @@
+// Code generated by cmd/lexgen (see Makefile's lexgen); DO NOT EDIT.
+
+// Lexicon schema: place.stream.media.createUpload
+
+package streamplace
+
+import (
+ "context"
+
+ lexutil "github.com/bluesky-social/indigo/lex/util"
+)
+
+// MediaCreateUpload_Input is the input argument to a place.stream.media.createUpload call.
+type MediaCreateUpload_Input struct {
+ // filename: Optional filename hint to attach as upload metadata.
+ Filename *string `json:"filename,omitempty" cborgen:"filename,omitempty"`
+ // mimeType: MIME type of the content being uploaded (e.g. video/mp4).
+ MimeType string `json:"mimeType" cborgen:"mimeType"`
+ // size: Total size of the upload in bytes.
+ Size int64 `json:"size" cborgen:"size"`
+}
+
+// MediaCreateUpload_Output is the output of a place.stream.media.createUpload call.
+type MediaCreateUpload_Output struct {
+ // expiresAt: When the upload token expires.
+ ExpiresAt string `json:"expiresAt" cborgen:"expiresAt"`
+ // uploadId: Server-side identifier for this upload.
+ UploadId string `json:"uploadId" cborgen:"uploadId"`
+ // uploadToken: Short-lived bearer token bound to this upload. Send as 'Authorization: Bearer ' on TUS requests.
+ UploadToken string `json:"uploadToken" cborgen:"uploadToken"`
+ // uploadUrl: Absolute URL to PATCH/HEAD per the TUS protocol.
+ UploadUrl string `json:"uploadUrl" cborgen:"uploadUrl"`
+}
+
+// MediaCreateUpload calls the XRPC method "place.stream.media.createUpload".
+func MediaCreateUpload(ctx context.Context, c lexutil.LexClient, input *MediaCreateUpload_Input) (*MediaCreateUpload_Output, error) {
+ var out MediaCreateUpload_Output
+ if err := c.LexDo(ctx, lexutil.Procedure, "application/json", "place.stream.media.createUpload", nil, input, &out); err != nil {
+ return nil, err
+ }
+
+ return &out, nil
+}
diff --git a/pkg/upload/upload.go b/pkg/upload/upload.go
new file mode 100644
index 00000000..7ab4f7d5
--- /dev/null
+++ b/pkg/upload/upload.go
@@ -0,0 +1,373 @@
+// Package upload implements resumable user uploads of arbitrary media
+// content using the TUS protocol. Uploads are pre-created by the
+// place.stream.media.createUpload XRPC (DPoP-authenticated) which returns a
+// URL and a short-lived bearer token; clients then PATCH/HEAD chunks against
+// the URL with only the bearer token, avoiding per-chunk DPoP.
+//
+// Two storage backends are supported. When S3 is configured, uploads are
+// streamed directly into an S3 multipart upload, which lets any node in a
+// station resume any upload. Otherwise uploads land on local disk under
+// $DataDir/uploads — that mode requires a single-node deployment.
+package upload
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "net/http"
+ "os"
+ "strings"
+ "time"
+
+ "github.com/aws/aws-sdk-go-v2/aws"
+ "github.com/aws/aws-sdk-go-v2/credentials"
+ awss3 "github.com/aws/aws-sdk-go-v2/service/s3"
+ "github.com/google/uuid"
+ "github.com/lestrrat-go/jwx/v2/jwa"
+ "github.com/lestrrat-go/jwx/v2/jwt"
+ tusfilestore "github.com/tus/tusd/v2/pkg/filestore"
+ tushandler "github.com/tus/tusd/v2/pkg/handler"
+ tusmemlocker "github.com/tus/tusd/v2/pkg/memorylocker"
+ tuss3store "github.com/tus/tusd/v2/pkg/s3store"
+
+ "stream.place/streamplace/pkg/config"
+ "stream.place/streamplace/pkg/log"
+ "stream.place/streamplace/pkg/statedb"
+)
+
+// Backend identifies which TUS data store backs an upload.
+type Backend string
+
+const (
+ BackendFile Backend = "file"
+ BackendS3 Backend = "s3"
+)
+
+// TokenLifetime is how long an upload bearer token remains valid. Renewed
+// uploads (long uploads, paused & resumed) will need a fresh token via
+// createUpload — we deliberately keep this finite to limit token leak blast
+// radius.
+const TokenLifetime = 24 * time.Hour
+
+// MaxUploadSize caps the upload size at the protocol layer. Set to zero to
+// disable enforcement. Currently 50 GiB.
+const MaxUploadSize int64 = 50 * 1024 * 1024 * 1024
+
+// BasePath is the URL prefix the TUS handler is mounted at. Token claims
+// embed the upload ID; URLs returned to clients are constructed from this
+// prefix plus the assigned ID.
+const BasePath = "/api/upload/"
+
+const tokenAudience = "streamplace-upload"
+
+// Manager owns the TUS store, the unrouted handler, and the bearer-token
+// signing key. It's safe to use as a singleton across the lifetime of the
+// server.
+type Manager struct {
+ cli *config.CLI
+ state *statedb.StatefulDB
+ composer *tushandler.StoreComposer
+ handler *tushandler.UnroutedHandler
+ backend Backend
+ authKey []byte
+}
+
+// New constructs an upload Manager, choosing the storage backend based on
+// the CLI's S3 configuration. Returns an error when configured for a
+// multi-node station without S3 — file-backed uploads cannot survive being
+// served by a different node than the one that issued the upload URL.
+func New(ctx context.Context, cli *config.CLI, state *statedb.StatefulDB) (*Manager, error) {
+ authKey, err := state.GetOrCreateUploadAuthKey()
+ if err != nil {
+ return nil, fmt.Errorf("upload: get auth key: %w", err)
+ }
+
+ multiNode := cli.ServerHost != "" && cli.BroadcasterHost != "" && cli.ServerHost != cli.BroadcasterHost
+ if multiNode && !cli.S3Configured() {
+ return nil, errors.New("upload: multi-node deployments require S3 storage; file-backed uploads are single-node only")
+ }
+
+ composer := tushandler.NewStoreComposer()
+ tusmemlocker.New().UseIn(composer)
+
+ var backend Backend
+ if cli.S3Configured() {
+ backend = BackendS3
+ s3client := awss3.New(awss3.Options{
+ Region: cli.S3Region,
+ Credentials: credentials.NewStaticCredentialsProvider(
+ cli.S3AccessKeyID,
+ cli.S3SecretAccessKey,
+ "",
+ ),
+ BaseEndpoint: aws.String(cli.S3Endpoint),
+ UsePathStyle: true,
+ })
+ store := tuss3store.New(cli.S3Bucket, s3client)
+ store.ObjectPrefix = "uploads/"
+ store.UseIn(composer)
+ log.Log(ctx, "upload manager: S3 backend", "bucket", cli.S3Bucket)
+ } else {
+ backend = BackendFile
+ dir := cli.DataFilePath([]string{"uploads"})
+ if err := os.MkdirAll(dir, 0o755); err != nil {
+ return nil, fmt.Errorf("upload: create upload dir %q: %w", dir, err)
+ }
+ store := tusfilestore.New(dir)
+ store.UseIn(composer)
+ log.Log(ctx, "upload manager: file backend", "dir", dir)
+ }
+
+ cfg := tushandler.Config{
+ StoreComposer: composer,
+ BasePath: BasePath,
+ MaxSize: MaxUploadSize,
+ NotifyCompleteUploads: true,
+ // We pre-create uploads from the createUpload XRPC, so the protocol's
+ // POST path is unused. Disabling download/termination keeps the
+ // surface small; if we need cancellation we can flip DisableTermination.
+ DisableDownload: true,
+ DisableTermination: false,
+ }
+ h, err := tushandler.NewUnroutedHandler(cfg)
+ if err != nil {
+ return nil, fmt.Errorf("upload: create tusd handler: %w", err)
+ }
+
+ m := &Manager{
+ cli: cli,
+ state: state,
+ composer: composer,
+ handler: h,
+ backend: backend,
+ authKey: authKey,
+ }
+
+ go m.notifyLoop(ctx)
+ return m, nil
+}
+
+// CreateResult is what the createUpload XRPC returns to the caller, modulo
+// JSON field naming.
+type CreateResult struct {
+ UploadID string
+ UploadURL string
+ UploadToken string
+ ExpiresAt time.Time
+}
+
+// Create pre-creates a TUS upload for the given user and mints a bearer
+// token bound to it. baseURL should be the absolute scheme+host the caller
+// reached (e.g. "https://example.com") so the returned UploadURL is
+// reachable for the client.
+func (m *Manager) Create(ctx context.Context, did string, mimeType string, filename string, size int64, baseURL string) (*CreateResult, error) {
+ if size <= 0 {
+ return nil, errors.New("upload: size must be positive")
+ }
+ if MaxUploadSize > 0 && size > MaxUploadSize {
+ return nil, fmt.Errorf("upload: size %d exceeds max %d", size, MaxUploadSize)
+ }
+
+ id, err := uuid.NewV7()
+ if err != nil {
+ return nil, fmt.Errorf("upload: generate id: %w", err)
+ }
+
+ meta := tushandler.MetaData{
+ "did": did,
+ "mimeType": mimeType,
+ }
+ if filename != "" {
+ meta["filename"] = filename
+ }
+ info := tushandler.FileInfo{
+ ID: id.String(),
+ Size: size,
+ MetaData: meta,
+ }
+
+ upload, err := m.composer.Core.NewUpload(ctx, info)
+ if err != nil {
+ return nil, fmt.Errorf("upload: create tusd upload: %w", err)
+ }
+ final, err := upload.GetInfo(ctx)
+ if err != nil {
+ return nil, fmt.Errorf("upload: read back upload info: %w", err)
+ }
+
+ row := &statedb.Upload{
+ ID: final.ID,
+ RepoDID: did,
+ MimeType: mimeType,
+ Filename: filename,
+ Size: size,
+ Backend: string(m.backend),
+ }
+ if final.Storage != nil {
+ // Filestore stores "Path"; s3store stores "Bucket"+"Key".
+ if p, ok := final.Storage["Path"]; ok {
+ row.Location = p
+ } else if k, ok := final.Storage["Key"]; ok {
+ row.Location = "s3://" + final.Storage["Bucket"] + "/" + k
+ }
+ }
+ if err := m.state.CreateUpload(ctx, row); err != nil {
+ return nil, fmt.Errorf("upload: persist upload row: %w", err)
+ }
+
+ expiresAt := time.Now().Add(TokenLifetime)
+ tok, err := m.mintToken(did, final.ID, expiresAt)
+ if err != nil {
+ return nil, fmt.Errorf("upload: mint token: %w", err)
+ }
+
+ url := strings.TrimRight(baseURL, "/") + BasePath + final.ID
+ return &CreateResult{
+ UploadID: final.ID,
+ UploadURL: url,
+ UploadToken: tok,
+ ExpiresAt: expiresAt,
+ }, nil
+}
+
+// ServeHTTP implements http.Handler for TUS requests. The caller is
+// responsible for routing TUS-protocol methods (HEAD/PATCH/DELETE) to this
+// handler at paths under BasePath. POST is rejected — uploads are
+// pre-created exclusively via createUpload.
+func (m *Manager) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+ if r.Method == http.MethodPost {
+ http.Error(w, "POST not supported; call place.stream.media.createUpload to start an upload", http.StatusMethodNotAllowed)
+ return
+ }
+
+ id := strings.TrimPrefix(r.URL.Path, BasePath)
+ id = strings.Trim(id, "/")
+ if id == "" {
+ http.Error(w, "missing upload id in path", http.StatusBadRequest)
+ return
+ }
+
+ if err := m.checkAuth(r, id); err != nil {
+ log.Warn(r.Context(), "upload auth rejected", "error", err, "uploadId", id)
+ http.Error(w, "unauthorized", http.StatusUnauthorized)
+ return
+ }
+
+ // tusd's handler reads the upload ID via strings.Trim(r.URL.Path, "/"),
+ // so we need to strip BasePath before delegating. Cloning the request is
+ // safer than mutating r.URL in-place.
+ r2 := r.Clone(r.Context())
+ r2.URL.Path = "/" + id
+
+ wrapped := m.handler.Middleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ switch r.Method {
+ case http.MethodHead:
+ m.handler.HeadFile(w, r)
+ case http.MethodPatch:
+ m.handler.PatchFile(w, r)
+ case http.MethodDelete:
+ m.handler.DelFile(w, r)
+ case http.MethodOptions:
+ // Middleware handles CORS; just terminate.
+ w.WriteHeader(http.StatusOK)
+ default:
+ w.Header().Set("Allow", "HEAD, PATCH, DELETE, OPTIONS")
+ w.WriteHeader(http.StatusMethodNotAllowed)
+ }
+ }))
+ wrapped.ServeHTTP(w, r2)
+}
+
+// notifyLoop consumes upload completion events from tusd's hook channel and
+// enqueues a VOD processing task for each finished upload.
+func (m *Manager) notifyLoop(ctx context.Context) {
+ for {
+ select {
+ case <-ctx.Done():
+ return
+ case ev := <-m.handler.CompleteUploads:
+ m.onComplete(ctx, ev)
+ }
+ }
+}
+
+func (m *Manager) onComplete(ctx context.Context, ev tushandler.HookEvent) {
+ id := ev.Upload.ID
+ ctx = log.WithLogValues(ctx, "uploadId", id)
+ log.Log(ctx, "upload complete", "size", ev.Upload.Size)
+
+ location := ""
+ if ev.Upload.Storage != nil {
+ if p, ok := ev.Upload.Storage["Path"]; ok {
+ location = p
+ } else if k, ok := ev.Upload.Storage["Key"]; ok {
+ location = "s3://" + ev.Upload.Storage["Bucket"] + "/" + k
+ }
+ }
+ if err := m.state.CompleteUpload(ctx, id, location); err != nil {
+ log.Error(ctx, "failed to mark upload complete", "error", err)
+ }
+
+ row, err := m.state.GetUpload(ctx, id)
+ if err != nil || row == nil {
+ log.Error(ctx, "failed to look up completed upload", "error", err)
+ return
+ }
+ payload := statedb.VODProcessTask{
+ UploadID: row.ID,
+ RepoDID: row.RepoDID,
+ MimeType: row.MimeType,
+ Filename: row.Filename,
+ Size: row.Size,
+ Backend: row.Backend,
+ Location: row.Location,
+ }
+ if _, err := m.state.EnqueueTask(ctx, statedb.TaskVODProcess, payload, statedb.WithTaskKey("vod-process:"+row.ID)); err != nil {
+ log.Error(ctx, "failed to enqueue vod-process task", "error", err)
+ }
+}
+
+func (m *Manager) mintToken(did, uploadID string, expiresAt time.Time) (string, error) {
+ now := time.Now()
+ tok, err := jwt.NewBuilder().
+ Issuer("streamplace").
+ Audience([]string{tokenAudience}).
+ Subject(did).
+ IssuedAt(now).
+ Expiration(expiresAt).
+ Claim("upload_id", uploadID).
+ Build()
+ if err != nil {
+ return "", err
+ }
+ signed, err := jwt.Sign(tok, jwt.WithKey(jwa.HS256, m.authKey))
+ if err != nil {
+ return "", err
+ }
+ return string(signed), nil
+}
+
+func (m *Manager) checkAuth(r *http.Request, uploadID string) error {
+ auth := r.Header.Get("Authorization")
+ if !strings.HasPrefix(auth, "Bearer ") {
+ return errors.New("missing bearer token")
+ }
+ raw := strings.TrimPrefix(auth, "Bearer ")
+ tok, err := jwt.Parse([]byte(raw),
+ jwt.WithKey(jwa.HS256, m.authKey),
+ jwt.WithValidate(true),
+ jwt.WithAudience(tokenAudience),
+ )
+ if err != nil {
+ return fmt.Errorf("parse token: %w", err)
+ }
+ v, ok := tok.Get("upload_id")
+ if !ok {
+ return errors.New("token missing upload_id claim")
+ }
+ claimID, ok := v.(string)
+ if !ok || claimID != uploadID {
+ return errors.New("token upload_id does not match request")
+ }
+ return nil
+}