An atproto PDS written in Go

Compare changes

Choose any two refs to compare.

Changed files
+1016 -581
cmd
cocoon
internal
db
metrics
models
oauth
client
dpop
server
templates
sqlite_blockstore
+7
cmd/cocoon/main.go
···
"os"
"time"
"github.com/bluesky-social/indigo/atproto/atcrypto"
"github.com/bluesky-social/indigo/atproto/syntax"
"github.com/haileyok/cocoon/internal/helpers"
···
Name: "fallback-proxy",
EnvVars: []string{"COCOON_FALLBACK_PROXY"},
},
},
Commands: []*cli.Command{
runServe,
···
Flags: []cli.Flag{},
Action: func(cmd *cli.Context) error {
s, err := server.New(&server.Args{
Addr: cmd.String("addr"),
DbName: cmd.String("db-name"),
DbType: cmd.String("db-type"),
···
"os"
"time"
+
"github.com/bluesky-social/go-util/pkg/telemetry"
"github.com/bluesky-social/indigo/atproto/atcrypto"
"github.com/bluesky-social/indigo/atproto/syntax"
"github.com/haileyok/cocoon/internal/helpers"
···
Name: "fallback-proxy",
EnvVars: []string{"COCOON_FALLBACK_PROXY"},
},
+
telemetry.CLIFlagDebug,
+
telemetry.CLIFlagMetricsListenAddress,
},
Commands: []*cli.Command{
runServe,
···
Flags: []cli.Flag{},
Action: func(cmd *cli.Context) error {
+
logger := telemetry.StartLogger(cmd)
+
telemetry.StartMetrics(cmd)
+
s, err := server.New(&server.Args{
+
Logger: logger,
Addr: cmd.String("addr"),
DbName: cmd.String("db-name"),
DbType: cmd.String("db-type"),
+19 -17
go.mod
···
module github.com/haileyok/cocoon
-
go 1.24.1
require (
github.com/Azure/go-autorest/autorest/to v0.4.1
github.com/aws/aws-sdk-go v1.55.7
github.com/bluesky-social/indigo v0.0.0-20251009212240-20524de167fe
github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792
github.com/domodwyer/mailyak/v3 v3.6.2
github.com/go-pkgz/expirable-cache/v3 v3.0.0
github.com/go-playground/validator v9.31.0+incompatible
github.com/golang-jwt/jwt/v4 v4.5.2
-
github.com/google/uuid v1.4.0
github.com/gorilla/sessions v1.4.0
github.com/gorilla/websocket v1.5.1
github.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b
···
github.com/joho/godotenv v1.5.1
github.com/labstack/echo-contrib v0.17.4
github.com/labstack/echo/v4 v4.13.3
-
github.com/lestrrat-go/jwx/v2 v2.0.12
github.com/multiformats/go-multihash v0.2.3
github.com/samber/slog-echo v1.16.1
github.com/urfave/cli/v2 v2.27.6
github.com/whyrusleeping/cbor-gen v0.2.1-0.20241030202151-b7a6831be65e
gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b
-
golang.org/x/crypto v0.38.0
gorm.io/driver/sqlite v1.5.7
gorm.io/gorm v1.25.12
)
···
github.com/gorilla/securecookie v1.1.2 // indirect
github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
-
github.com/hashicorp/go-retryablehttp v0.7.5 // indirect
github.com/hashicorp/golang-lru v1.0.2 // indirect
github.com/ipfs/bbloom v0.0.4 // indirect
github.com/ipfs/go-blockservice v0.5.2 // indirect
···
github.com/ipld/go-ipld-prime v0.21.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
-
github.com/jackc/pgx/v5 v5.5.0 // indirect
github.com/jackc/puddle/v2 v2.2.1 // indirect
github.com/jbenet/goprocess v0.1.4 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
···
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
github.com/labstack/gommon v0.4.2 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
-
github.com/lestrrat-go/blackmagic v1.0.1 // indirect
github.com/lestrrat-go/httpcc v1.0.1 // indirect
-
github.com/lestrrat-go/httprc v1.0.4 // indirect
github.com/lestrrat-go/iter v1.0.2 // indirect
github.com/lestrrat-go/option v1.0.1 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
···
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/opentracing/opentracing-go v1.2.0 // indirect
github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f // indirect
-
github.com/prometheus/client_golang v1.22.0 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
-
github.com/prometheus/common v0.63.0 // indirect
github.com/prometheus/procfs v0.16.1 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/samber/lo v1.49.1 // indirect
···
github.com/valyala/fasttemplate v1.2.2 // indirect
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02 // indirect
-
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 // indirect
go.opentelemetry.io/otel v1.29.0 // indirect
go.opentelemetry.io/otel/metric v1.29.0 // indirect
go.opentelemetry.io/otel/trace v1.29.0 // indirect
go.uber.org/atomic v1.11.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.26.0 // indirect
-
golang.org/x/net v0.40.0 // indirect
-
golang.org/x/sync v0.14.0 // indirect
-
golang.org/x/sys v0.33.0 // indirect
-
golang.org/x/text v0.25.0 // indirect
golang.org/x/time v0.11.0 // indirect
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect
-
google.golang.org/protobuf v1.36.6 // indirect
gopkg.in/go-playground/assert.v1 v1.2.1 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
-
gorm.io/driver/postgres v1.5.7 // indirect
lukechampine.com/blake3 v1.2.1 // indirect
)
···
module github.com/haileyok/cocoon
+
go 1.24.5
require (
github.com/Azure/go-autorest/autorest/to v0.4.1
github.com/aws/aws-sdk-go v1.55.7
+
github.com/bluesky-social/go-util v0.0.0-20251012040650-2ebbf57f5934
github.com/bluesky-social/indigo v0.0.0-20251009212240-20524de167fe
github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792
github.com/domodwyer/mailyak/v3 v3.6.2
github.com/go-pkgz/expirable-cache/v3 v3.0.0
github.com/go-playground/validator v9.31.0+incompatible
github.com/golang-jwt/jwt/v4 v4.5.2
+
github.com/google/uuid v1.6.0
github.com/gorilla/sessions v1.4.0
github.com/gorilla/websocket v1.5.1
github.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b
···
github.com/joho/godotenv v1.5.1
github.com/labstack/echo-contrib v0.17.4
github.com/labstack/echo/v4 v4.13.3
+
github.com/lestrrat-go/jwx/v2 v2.0.21
github.com/multiformats/go-multihash v0.2.3
+
github.com/prometheus/client_golang v1.23.2
github.com/samber/slog-echo v1.16.1
github.com/urfave/cli/v2 v2.27.6
github.com/whyrusleeping/cbor-gen v0.2.1-0.20241030202151-b7a6831be65e
gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b
+
golang.org/x/crypto v0.41.0
+
gorm.io/driver/postgres v1.5.7
gorm.io/driver/sqlite v1.5.7
gorm.io/gorm v1.25.12
)
···
github.com/gorilla/securecookie v1.1.2 // indirect
github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
+
github.com/hashicorp/go-retryablehttp v0.7.7 // indirect
github.com/hashicorp/golang-lru v1.0.2 // indirect
github.com/ipfs/bbloom v0.0.4 // indirect
github.com/ipfs/go-blockservice v0.5.2 // indirect
···
github.com/ipld/go-ipld-prime v0.21.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
+
github.com/jackc/pgx/v5 v5.5.4 // indirect
github.com/jackc/puddle/v2 v2.2.1 // indirect
github.com/jbenet/goprocess v0.1.4 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
···
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
github.com/labstack/gommon v0.4.2 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
+
github.com/lestrrat-go/blackmagic v1.0.2 // indirect
github.com/lestrrat-go/httpcc v1.0.1 // indirect
+
github.com/lestrrat-go/httprc v1.0.5 // indirect
github.com/lestrrat-go/iter v1.0.2 // indirect
github.com/lestrrat-go/option v1.0.1 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
···
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/opentracing/opentracing-go v1.2.0 // indirect
github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f // indirect
github.com/prometheus/client_model v0.6.2 // indirect
+
github.com/prometheus/common v0.66.1 // indirect
github.com/prometheus/procfs v0.16.1 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/samber/lo v1.49.1 // indirect
···
github.com/valyala/fasttemplate v1.2.2 // indirect
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02 // indirect
+
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect
go.opentelemetry.io/otel v1.29.0 // indirect
go.opentelemetry.io/otel/metric v1.29.0 // indirect
go.opentelemetry.io/otel/trace v1.29.0 // indirect
go.uber.org/atomic v1.11.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.26.0 // indirect
+
go.yaml.in/yaml/v2 v2.4.2 // indirect
+
golang.org/x/net v0.43.0 // indirect
+
golang.org/x/sync v0.16.0 // indirect
+
golang.org/x/sys v0.35.0 // indirect
+
golang.org/x/text v0.28.0 // indirect
golang.org/x/time v0.11.0 // indirect
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect
+
google.golang.org/protobuf v1.36.9 // indirect
gopkg.in/go-playground/assert.v1 v1.2.1 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
lukechampine.com/blake3 v1.2.1 // indirect
)
+50 -74
go.sum
···
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932 h1:mXoPYz/Ul5HYEDvkta6I8/rnYM5gSdSV2tJ6XbZuEtY=
github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932/go.mod h1:NOuUCSz6Q9T7+igc/hlvDOUdtWKryOrtFyIVABv/p7k=
github.com/bluesky-social/indigo v0.0.0-20251009212240-20524de167fe h1:VBhaqE5ewQgXbY5SfSWFZC/AwHFo7cHxZKFYi2ce9Yo=
github.com/bluesky-social/indigo v0.0.0-20251009212240-20524de167fe/go.mod h1:RuQVrCGm42QNsgumKaR6se+XkFKfCPNwdCiTvqKRUck=
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY=
···
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-
github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0=
github.com/domodwyer/mailyak/v3 v3.6.2 h1:x3tGMsyFhTCaxp6ycgR0FE/bu5QiNp+hetUuCOBXMn8=
github.com/domodwyer/mailyak/v3 v3.6.2/go.mod h1:lOm/u9CyCVWHeaAmHIdF4RiKVxKUT/H5XX10lIKAL6c=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
···
github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8=
github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
-
github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4=
-
github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/context v1.1.2 h1:WRkNAv2uoa03QNIc1A6u4O7DAGMUVoopZhkiXWA2V1o=
···
github.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b/go.mod h1:VzxiSdG6j1pi7rwGm/xYI5RbtpBgM8sARDXlvEvxlu0=
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
-
github.com/hashicorp/go-hclog v0.9.2 h1:CG6TE5H9/JXsFWJCfoIVpKFIkFe6ysEuHirp4DxCsHI=
-
github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
-
github.com/hashicorp/go-retryablehttp v0.7.5 h1:bJj+Pj19UZMIweq/iie+1u5YCdGrnxCT9yvm0e+Nd5M=
-
github.com/hashicorp/go-retryablehttp v0.7.5/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5wjtH1ewM9u8iYVjtX8=
github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c=
github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
···
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
-
github.com/jackc/pgx/v5 v5.5.0 h1:NxstgwndsTRy7eq9/kqYc/BZh5w2hHJV86wjvO+1xPw=
-
github.com/jackc/pgx/v5 v5.5.0/go.mod h1:Ig06C2Vu0t5qXC60W8sqIthScaEnFvojjj9dSljmHRA=
github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus=
···
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/koron/go-ssdp v0.0.3 h1:JivLMY45N76b4p/vsWGOKewBQu6uf39y8l+AQ7sDKx8=
···
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/labstack/echo-contrib v0.17.4 h1:g5mfsrJfJTKv+F5uNKCyrjLK7js+ZW6HTjg4FnDxxgk=
github.com/labstack/echo-contrib v0.17.4/go.mod h1:9O7ZPAHUeMGTOAfg80YqQduHzt0CzLak36PZRldYrZ0=
github.com/labstack/echo/v4 v4.13.3 h1:pwhpCPrTl5qry5HRdM5FwdXnhXSLSY+WE+YQSeCaafY=
···
github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
-
github.com/lestrrat-go/blackmagic v1.0.1 h1:lS5Zts+5HIC/8og6cGHb0uCcNCa3OUt1ygh3Qz2Fe80=
-
github.com/lestrrat-go/blackmagic v1.0.1/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU=
github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE=
github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E=
-
github.com/lestrrat-go/httprc v1.0.4 h1:bAZymwoZQb+Oq8MEbyipag7iSq6YIga8Wj6GOiJGdI8=
-
github.com/lestrrat-go/httprc v1.0.4/go.mod h1:mwwz3JMTPBjHUkkDv/IGJ39aALInZLrhBp0X7KGUZlo=
github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI=
github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4=
-
github.com/lestrrat-go/jwx/v2 v2.0.12 h1:3d589+5w/b9b7S3DneICPW16AqTyYXB7VRjgluSDWeA=
-
github.com/lestrrat-go/jwx/v2 v2.0.12/go.mod h1:Mq4KN1mM7bp+5z/W5HS8aCNs5RKZ911G/0y2qUjAQuQ=
-
github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU=
github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
github.com/libp2p/go-buffer-pool v0.1.0 h1:oK4mSFcQz7cTQIfqbe4MIj9gLW+mnanjyFtc6cdF0Y8=
···
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f h1:VXTQfuJj9vKR4TCkEuWIckKvdHFeJH/huIFJ9/cXOB0=
github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f/go.mod h1:/zvteZs/GwLtCgZ4BL6CBsk9IKIlexP43ObX9AxTqTw=
-
github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
-
github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
-
github.com/prometheus/common v0.63.0 h1:YR/EIY1o3mEFP/kZCD7iDMnLPlGyuU2Gb3HIcXnA98k=
-
github.com/prometheus/common v0.63.0/go.mod h1:VVFF/fBIoToEnWRVkYoXEkq3R3paCoxG9PXP74SnV18=
github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
···
github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
-
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
-
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
-
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
-
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
-
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
-
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
-
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/urfave/cli v1.22.10/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
github.com/urfave/cli/v2 v2.27.6 h1:VdRdS98FNhKZ8/Az8B7MTyGQmpIr36O1EHybx/LaZ4g=
github.com/urfave/cli/v2 v2.27.6/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ=
···
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
-
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b h1:CzigHMRySiX3drau9C6Q5CAbNIApmLdat5jPMqChvDA=
gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b/go.mod h1:/y/V339mxv2sZmYYR64O07VuCpdNZqCTwO8ZcouTMI8=
gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02 h1:qwDnMxjkyLmAFgcfgTnfJrmYKWhHnci3GjDqcZp1M3Q=
gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02/go.mod h1:JTnUj0mpYiAsuZLmKjTx/ex3AtMowcCgnE7YNyCEP0I=
-
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 h1:aFJWCqJMNjENlcleuuOkGAPH82y0yULBScfXcIEdS24=
-
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1/go.mod h1:sEGXWArGqc3tVa+ekntsN65DmVbVeW+7lTKTjZF3/Fo=
go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw=
go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8=
go.opentelemetry.io/otel/metric v1.29.0 h1:vPf/HFWTNkPu1aYeIsc98l4ktOQaL6LeSoeV2g+8YLc=
···
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
-
go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk=
-
go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo=
go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
···
go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI=
go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo=
go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
-
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
-
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
-
golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
-
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa h1:FRnLl4eNAQl8hwxVVC17teOw8kdjVDVAiFMtgUdTSRQ=
golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa/go.mod h1:zk2irFbV9DP96SEBUUAy67IdHUaZuSnrz1n472HUCLE=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
···
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
-
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
-
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
-
golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
-
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
-
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
-
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
-
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
-
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
-
golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
-
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-
golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
-
golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
-
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
-
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
-
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
-
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
-
golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
-
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
-
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
-
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
-
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
-
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
···
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
-
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
-
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
-
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=
-
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU=
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
-
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
-
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
···
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932 h1:mXoPYz/Ul5HYEDvkta6I8/rnYM5gSdSV2tJ6XbZuEtY=
github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932/go.mod h1:NOuUCSz6Q9T7+igc/hlvDOUdtWKryOrtFyIVABv/p7k=
+
github.com/bluesky-social/go-util v0.0.0-20251012040650-2ebbf57f5934 h1:btHMur2kTRgWEnCHn6LaI3BE9YRgsqTpwpJ1UdB7VEk=
+
github.com/bluesky-social/go-util v0.0.0-20251012040650-2ebbf57f5934/go.mod h1:LWamyZfbQGW7PaVc5jumFfjgrshJ5mXgDUnR6fK7+BI=
github.com/bluesky-social/indigo v0.0.0-20251009212240-20524de167fe h1:VBhaqE5ewQgXbY5SfSWFZC/AwHFo7cHxZKFYi2ce9Yo=
github.com/bluesky-social/indigo v0.0.0-20251009212240-20524de167fe/go.mod h1:RuQVrCGm42QNsgumKaR6se+XkFKfCPNwdCiTvqKRUck=
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY=
···
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0=
github.com/domodwyer/mailyak/v3 v3.6.2 h1:x3tGMsyFhTCaxp6ycgR0FE/bu5QiNp+hetUuCOBXMn8=
github.com/domodwyer/mailyak/v3 v3.6.2/go.mod h1:lOm/u9CyCVWHeaAmHIdF4RiKVxKUT/H5XX10lIKAL6c=
+
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
+
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
···
github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8=
github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
+
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/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/context v1.1.2 h1:WRkNAv2uoa03QNIc1A6u4O7DAGMUVoopZhkiXWA2V1o=
···
github.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b/go.mod h1:VzxiSdG6j1pi7rwGm/xYI5RbtpBgM8sARDXlvEvxlu0=
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
+
github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=
+
github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
+
github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU=
+
github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk=
github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c=
github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
···
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
+
github.com/jackc/pgx/v5 v5.5.4 h1:Xp2aQS8uXButQdnCMWNmvx6UysWQQC+u1EoizjguY+8=
+
github.com/jackc/pgx/v5 v5.5.4/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A=
github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus=
···
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
+
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
+
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/koron/go-ssdp v0.0.3 h1:JivLMY45N76b4p/vsWGOKewBQu6uf39y8l+AQ7sDKx8=
···
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
+
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
+
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/labstack/echo-contrib v0.17.4 h1:g5mfsrJfJTKv+F5uNKCyrjLK7js+ZW6HTjg4FnDxxgk=
github.com/labstack/echo-contrib v0.17.4/go.mod h1:9O7ZPAHUeMGTOAfg80YqQduHzt0CzLak36PZRldYrZ0=
github.com/labstack/echo/v4 v4.13.3 h1:pwhpCPrTl5qry5HRdM5FwdXnhXSLSY+WE+YQSeCaafY=
···
github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
+
github.com/lestrrat-go/blackmagic v1.0.2 h1:Cg2gVSc9h7sz9NOByczrbUvLopQmXrfFx//N+AkAr5k=
+
github.com/lestrrat-go/blackmagic v1.0.2/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU=
github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE=
github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E=
+
github.com/lestrrat-go/httprc v1.0.5 h1:bsTfiH8xaKOJPrg1R+E3iE/AWZr/x0Phj9PBTG/OLUk=
+
github.com/lestrrat-go/httprc v1.0.5/go.mod h1:mwwz3JMTPBjHUkkDv/IGJ39aALInZLrhBp0X7KGUZlo=
github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI=
github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4=
+
github.com/lestrrat-go/jwx/v2 v2.0.21 h1:jAPKupy4uHgrHFEdjVjNkUgoBKtVDgrQPB/h55FHrR0=
+
github.com/lestrrat-go/jwx/v2 v2.0.21/go.mod h1:09mLW8zto6bWL9GbwnqAli+ArLf+5M33QLQPDggkUWM=
github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU=
github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
github.com/libp2p/go-buffer-pool v0.1.0 h1:oK4mSFcQz7cTQIfqbe4MIj9gLW+mnanjyFtc6cdF0Y8=
···
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f h1:VXTQfuJj9vKR4TCkEuWIckKvdHFeJH/huIFJ9/cXOB0=
github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f/go.mod h1:/zvteZs/GwLtCgZ4BL6CBsk9IKIlexP43ObX9AxTqTw=
+
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.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
+
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.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
···
github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
+
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/urfave/cli v1.22.10/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
github.com/urfave/cli/v2 v2.27.6 h1:VdRdS98FNhKZ8/Az8B7MTyGQmpIr36O1EHybx/LaZ4g=
github.com/urfave/cli/v2 v2.27.6/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ=
···
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b h1:CzigHMRySiX3drau9C6Q5CAbNIApmLdat5jPMqChvDA=
gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b/go.mod h1:/y/V339mxv2sZmYYR64O07VuCpdNZqCTwO8ZcouTMI8=
gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02 h1:qwDnMxjkyLmAFgcfgTnfJrmYKWhHnci3GjDqcZp1M3Q=
gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02/go.mod h1:JTnUj0mpYiAsuZLmKjTx/ex3AtMowcCgnE7YNyCEP0I=
+
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk=
+
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw=
go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw=
go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8=
go.opentelemetry.io/otel/metric v1.29.0 h1:vPf/HFWTNkPu1aYeIsc98l4ktOQaL6LeSoeV2g+8YLc=
···
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
+
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
+
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
···
go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI=
go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo=
go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so=
+
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-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
+
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa h1:FRnLl4eNAQl8hwxVVC17teOw8kdjVDVAiFMtgUdTSRQ=
golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa/go.mod h1:zk2irFbV9DP96SEBUUAy67IdHUaZuSnrz1n472HUCLE=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
···
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+
golang.org/x/mod v0.26.0 h1:EGMPT//Ezu+ylkCijjPc+f4Aih7sZvaAr+O3EHBxvZg=
+
golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
+
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
+
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
+
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
+
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
+
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
···
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
+
golang.org/x/tools v0.35.0 h1:mBffYraMEf7aa0sB+NuKnuCy8qI/9Bughn8dC2Gu5r0=
+
golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU=
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
+
google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=
+
google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
+15 -14
internal/db/db.go
···
package db
import (
"sync"
"gorm.io/gorm"
···
}
}
-
func (db *DB) Create(value any, clauses []clause.Expression) *gorm.DB {
db.mu.Lock()
defer db.mu.Unlock()
-
return db.cli.Clauses(clauses...).Create(value)
}
-
func (db *DB) Save(value any, clauses []clause.Expression) *gorm.DB {
db.mu.Lock()
defer db.mu.Unlock()
-
return db.cli.Clauses(clauses...).Save(value)
}
-
func (db *DB) Exec(sql string, clauses []clause.Expression, values ...any) *gorm.DB {
db.mu.Lock()
defer db.mu.Unlock()
-
return db.cli.Clauses(clauses...).Exec(sql, values...)
}
-
func (db *DB) Raw(sql string, clauses []clause.Expression, values ...any) *gorm.DB {
-
return db.cli.Clauses(clauses...).Raw(sql, values...)
}
func (db *DB) AutoMigrate(models ...any) error {
return db.cli.AutoMigrate(models...)
}
-
func (db *DB) Delete(value any, clauses []clause.Expression) *gorm.DB {
db.mu.Lock()
defer db.mu.Unlock()
-
return db.cli.Clauses(clauses...).Delete(value)
}
-
func (db *DB) First(dest any, conds ...any) *gorm.DB {
-
return db.cli.First(dest, conds...)
}
// TODO: this isn't actually good. we can commit even if the db is locked here. this is probably okay for the time being, but need to figure
// out a better solution. right now we only do this whenever we're importing a repo though so i'm mostly not worried, but it's still bad.
// e.g. when we do apply writes we should also be using a transcation but we don't right now
-
func (db *DB) BeginDangerously() *gorm.DB {
-
return db.cli.Begin()
}
func (db *DB) Lock() {
···
package db
import (
+
"context"
"sync"
"gorm.io/gorm"
···
}
}
+
func (db *DB) Create(ctx context.Context, value any, clauses []clause.Expression) *gorm.DB {
db.mu.Lock()
defer db.mu.Unlock()
+
return db.cli.WithContext(ctx).Clauses(clauses...).Create(value)
}
+
func (db *DB) Save(ctx context.Context, value any, clauses []clause.Expression) *gorm.DB {
db.mu.Lock()
defer db.mu.Unlock()
+
return db.cli.WithContext(ctx).Clauses(clauses...).Save(value)
}
+
func (db *DB) Exec(ctx context.Context, sql string, clauses []clause.Expression, values ...any) *gorm.DB {
db.mu.Lock()
defer db.mu.Unlock()
+
return db.cli.WithContext(ctx).Clauses(clauses...).Exec(sql, values...)
}
+
func (db *DB) Raw(ctx context.Context, sql string, clauses []clause.Expression, values ...any) *gorm.DB {
+
return db.cli.WithContext(ctx).Clauses(clauses...).Raw(sql, values...)
}
func (db *DB) AutoMigrate(models ...any) error {
return db.cli.AutoMigrate(models...)
}
+
func (db *DB) Delete(ctx context.Context, value any, clauses []clause.Expression) *gorm.DB {
db.mu.Lock()
defer db.mu.Unlock()
+
return db.cli.WithContext(ctx).Clauses(clauses...).Delete(value)
}
+
func (db *DB) First(ctx context.Context, dest any, conds ...any) *gorm.DB {
+
return db.cli.WithContext(ctx).First(dest, conds...)
}
// TODO: this isn't actually good. we can commit even if the db is locked here. this is probably okay for the time being, but need to figure
// out a better solution. right now we only do this whenever we're importing a repo though so i'm mostly not worried, but it's still bad.
// e.g. when we do apply writes we should also be using a transcation but we don't right now
+
func (db *DB) BeginDangerously(ctx context.Context) *gorm.DB {
+
return db.cli.WithContext(ctx).Begin()
}
func (db *DB) Lock() {
+30
metrics/metrics.go
···
···
+
package metrics
+
+
import (
+
"github.com/prometheus/client_golang/prometheus"
+
"github.com/prometheus/client_golang/prometheus/promauto"
+
)
+
+
const (
+
NAMESPACE = "cocoon"
+
)
+
+
var (
+
RelaysConnected = promauto.NewGaugeVec(prometheus.GaugeOpts{
+
Namespace: NAMESPACE,
+
Name: "relays_connected",
+
Help: "number of connected relays, by host",
+
}, []string{"host"})
+
+
RelaySends = promauto.NewCounterVec(prometheus.CounterOpts{
+
Namespace: NAMESPACE,
+
Name: "relay_sends",
+
Help: "number of events sent to a relay, by host",
+
}, []string{"host", "kind"})
+
+
RepoOperations = promauto.NewCounterVec(prometheus.CounterOpts{
+
Namespace: NAMESPACE,
+
Name: "repo_operations",
+
Help: "number of operations made against repos",
+
}, []string{"kind"})
+
)
+12 -2
models/models.go
···
"github.com/bluesky-social/indigo/atproto/atcrypto"
)
type Repo struct {
Did string `gorm:"primaryKey"`
CreatedAt time.Time
···
Root []byte
Preferences []byte
Deactivated bool
}
func (r *Repo) SignFor(ctx context.Context, did string, msg []byte) ([]byte, error) {
···
}
type ReservedKey struct {
-
KeyDid string `gorm:"primaryKey"`
-
Did *string `gorm:"index"`
PrivateKey []byte
CreatedAt time.Time `gorm:"index"`
}
···
"github.com/bluesky-social/indigo/atproto/atcrypto"
)
+
type TwoFactorType string
+
+
var (
+
TwoFactorTypeNone = TwoFactorType("none")
+
TwoFactorTypeEmail = TwoFactorType("email")
+
)
+
type Repo struct {
Did string `gorm:"primaryKey"`
CreatedAt time.Time
···
Root []byte
Preferences []byte
Deactivated bool
+
TwoFactorCode *string
+
TwoFactorCodeExpiresAt *time.Time
+
TwoFactorType TwoFactorType `gorm:"default:none"`
}
func (r *Repo) SignFor(ctx context.Context, did string, msg []byte) ([]byte, error) {
···
}
type ReservedKey struct {
+
KeyDid string `gorm:"primaryKey"`
+
Did *string `gorm:"index"`
PrivateKey []byte
CreatedAt time.Time `gorm:"index"`
}
-1
oauth/client/manager.go
···
}
jwks = k
-
} else if metadata.JWKS != nil {
} else if metadata.JWKSURI != nil {
maybeJwks, err := cm.getClientJwks(ctx, clientId, *metadata.JWKSURI)
if err != nil {
···
}
jwks = k
} else if metadata.JWKSURI != nil {
maybeJwks, err := cm.getClientJwks(ctx, clientId, *metadata.JWKSURI)
if err != nil {
+1 -1
oauth/dpop/jti_cache.go
···
}
func newJTICache(size int) *jtiCache {
-
cache := cache.NewCache[string, bool]().WithTTL(24 * time.Hour).WithLRU().WithTTL(constants.JTITtl)
return &jtiCache{
cache: cache,
mu: sync.Mutex{},
···
}
func newJTICache(size int) *jtiCache {
+
cache := cache.NewCache[string, bool]().WithTTL(24 * time.Hour).WithLRU().WithTTL(constants.JTITtl).WithMaxKeys(size)
return &jtiCache{
cache: cache,
mu: sync.Mutex{},
+10 -8
server/common.go
···
package server
import (
"github.com/haileyok/cocoon/models"
)
-
func (s *Server) getActorByHandle(handle string) (*models.Actor, error) {
var actor models.Actor
-
if err := s.db.First(&actor, models.Actor{Handle: handle}).Error; err != nil {
return nil, err
}
return &actor, nil
}
-
func (s *Server) getRepoByEmail(email string) (*models.Repo, error) {
var repo models.Repo
-
if err := s.db.First(&repo, models.Repo{Email: email}).Error; err != nil {
return nil, err
}
return &repo, nil
}
-
func (s *Server) getRepoActorByEmail(email string) (*models.RepoActor, error) {
var repo models.RepoActor
-
if err := s.db.Raw("SELECT r.*, a.* FROM repos r LEFT JOIN actors a ON r.did = a.did WHERE r.email= ?", nil, email).Scan(&repo).Error; err != nil {
return nil, err
}
return &repo, nil
}
-
func (s *Server) getRepoActorByDid(did string) (*models.RepoActor, error) {
var repo models.RepoActor
-
if err := s.db.Raw("SELECT r.*, a.* FROM repos r LEFT JOIN actors a ON r.did = a.did WHERE r.did = ?", nil, did).Scan(&repo).Error; err != nil {
return nil, err
}
return &repo, nil
···
package server
import (
+
"context"
+
"github.com/haileyok/cocoon/models"
)
+
func (s *Server) getActorByHandle(ctx context.Context, handle string) (*models.Actor, error) {
var actor models.Actor
+
if err := s.db.First(ctx, &actor, models.Actor{Handle: handle}).Error; err != nil {
return nil, err
}
return &actor, nil
}
+
func (s *Server) getRepoByEmail(ctx context.Context, email string) (*models.Repo, error) {
var repo models.Repo
+
if err := s.db.First(ctx, &repo, models.Repo{Email: email}).Error; err != nil {
return nil, err
}
return &repo, nil
}
+
func (s *Server) getRepoActorByEmail(ctx context.Context, email string) (*models.RepoActor, error) {
var repo models.RepoActor
+
if err := s.db.Raw(ctx, "SELECT r.*, a.* FROM repos r LEFT JOIN actors a ON r.did = a.did WHERE r.email= ?", nil, email).Scan(&repo).Error; err != nil {
return nil, err
}
return &repo, nil
}
+
func (s *Server) getRepoActorByDid(ctx context.Context, did string) (*models.RepoActor, error) {
var repo models.RepoActor
+
if err := s.db.Raw(ctx, "SELECT r.*, a.* FROM repos r LEFT JOIN actors a ON r.did = a.did WHERE r.did = ?", nil, did).Scan(&repo).Error; err != nil {
return nil, err
}
return &repo, nil
+4 -2
server/handle_account.go
···
func (s *Server) handleAccount(e echo.Context) error {
ctx := e.Request().Context()
repo, sess, err := s.getSessionRepoOrErr(e)
if err != nil {
return e.Redirect(303, "/account/signin")
···
oldestPossibleSession := time.Now().Add(constants.ConfidentialClientSessionLifetime)
var tokens []provider.OauthToken
-
if err := s.db.Raw("SELECT * FROM oauth_tokens WHERE sub = ? AND created_at < ? ORDER BY created_at ASC", nil, repo.Repo.Did, oldestPossibleSession).Scan(&tokens).Error; err != nil {
-
s.logger.Error("couldnt fetch oauth sessions for account", "did", repo.Repo.Did, "error", err)
sess.AddFlash("Unable to fetch sessions. See server logs for more details.", "error")
sess.Save(e.Request(), e.Response())
return e.Render(200, "account.html", map[string]any{
···
func (s *Server) handleAccount(e echo.Context) error {
ctx := e.Request().Context()
+
logger := s.logger.With("name", "handleAuth")
+
repo, sess, err := s.getSessionRepoOrErr(e)
if err != nil {
return e.Redirect(303, "/account/signin")
···
oldestPossibleSession := time.Now().Add(constants.ConfidentialClientSessionLifetime)
var tokens []provider.OauthToken
+
if err := s.db.Raw(ctx, "SELECT * FROM oauth_tokens WHERE sub = ? AND created_at < ? ORDER BY created_at ASC", nil, repo.Repo.Did, oldestPossibleSession).Scan(&tokens).Error; err != nil {
+
logger.Error("couldnt fetch oauth sessions for account", "did", repo.Repo.Did, "error", err)
sess.AddFlash("Unable to fetch sessions. See server logs for more details.", "error")
sess.Save(e.Request(), e.Response())
return e.Render(200, "account.html", map[string]any{
+8 -5
server/handle_account_revoke.go
···
"github.com/labstack/echo/v4"
)
-
type AccountRevokeRequest struct {
Token string `form:"token"`
}
func (s *Server) handleAccountRevoke(e echo.Context) error {
-
var req AccountRevokeRequest
if err := e.Bind(&req); err != nil {
-
s.logger.Error("could not bind account revoke request", "error", err)
return helpers.ServerError(e, nil)
}
···
return e.Redirect(303, "/account/signin")
}
-
if err := s.db.Exec("DELETE FROM oauth_tokens WHERE sub = ? AND token = ?", nil, repo.Repo.Did, req.Token).Error; err != nil {
-
s.logger.Error("couldnt delete oauth session for account", "did", repo.Repo.Did, "token", req.Token, "error", err)
sess.AddFlash("Unable to revoke session. See server logs for more details.", "error")
sess.Save(e.Request(), e.Response())
return e.Redirect(303, "/account")
···
"github.com/labstack/echo/v4"
)
+
type AccountRevokeInput struct {
Token string `form:"token"`
}
func (s *Server) handleAccountRevoke(e echo.Context) error {
+
ctx := e.Request().Context()
+
logger := s.logger.With("name", "handleAcocuntRevoke")
+
+
var req AccountRevokeInput
if err := e.Bind(&req); err != nil {
+
logger.Error("could not bind account revoke request", "error", err)
return helpers.ServerError(e, nil)
}
···
return e.Redirect(303, "/account/signin")
}
+
if err := s.db.Exec(ctx, "DELETE FROM oauth_tokens WHERE sub = ? AND token = ?", nil, repo.Repo.Did, req.Token).Error; err != nil {
+
logger.Error("couldnt delete oauth session for account", "did", repo.Repo.Did, "token", req.Token, "error", err)
sess.AddFlash("Unable to revoke session. See server logs for more details.", "error")
sess.Save(e.Request(), e.Response())
return e.Redirect(303, "/account")
+68 -16
server/handle_account_signin.go
···
import (
"errors"
"strings"
"github.com/bluesky-social/indigo/atproto/syntax"
"github.com/gorilla/sessions"
···
"gorm.io/gorm"
)
-
type OauthSigninRequest struct {
-
Username string `form:"username"`
-
Password string `form:"password"`
-
QueryParams string `form:"query_params"`
}
func (s *Server) getSessionRepoOrErr(e echo.Context) (*models.RepoActor, *sessions.Session, error) {
sess, err := session.Get("session", e)
if err != nil {
return nil, nil, err
···
return nil, sess, errors.New("did was not set in session")
}
-
repo, err := s.getRepoActorByDid(did)
if err != nil {
return nil, sess, err
}
···
func getFlashesFromSession(e echo.Context, sess *sessions.Session) map[string]any {
defer sess.Save(e.Request(), e.Response())
return map[string]any{
-
"errors": sess.Flashes("error"),
-
"successes": sess.Flashes("success"),
}
}
···
}
func (s *Server) handleAccountSigninPost(e echo.Context) error {
-
var req OauthSigninRequest
if err := e.Bind(&req); err != nil {
-
s.logger.Error("error binding sign in req", "error", err)
return helpers.ServerError(e, nil)
}
···
idtype = "handle"
} else {
idtype = "email"
}
// TODO: we should make this a helper since we do it for the base create_session as well
···
var err error
switch idtype {
case "did":
-
err = s.db.Raw("SELECT r.*, a.* FROM repos r LEFT JOIN actors a ON r.did = a.did WHERE r.did = ?", nil, req.Username).Scan(&repo).Error
case "handle":
-
err = s.db.Raw("SELECT r.*, a.* FROM actors a LEFT JOIN repos r ON a.did = r.did WHERE a.handle = ?", nil, req.Username).Scan(&repo).Error
case "email":
-
err = s.db.Raw("SELECT r.*, a.* FROM repos r LEFT JOIN actors a ON r.did = a.did WHERE r.email = ?", nil, req.Username).Scan(&repo).Error
}
if err != nil {
if err == gorm.ErrRecordNotFound {
···
sess.AddFlash("Something went wrong!", "error")
}
sess.Save(e.Request(), e.Response())
-
return e.Redirect(303, "/account/signin")
}
if err := bcrypt.CompareHashAndPassword([]byte(repo.Password), []byte(req.Password)); err != nil {
···
sess.AddFlash("Something went wrong!", "error")
}
sess.Save(e.Request(), e.Response())
-
return e.Redirect(303, "/account/signin")
}
sess.Options = &sessions.Options{
···
return err
}
-
if req.QueryParams != "" {
-
return e.Redirect(303, "/oauth/authorize?"+req.QueryParams)
} else {
return e.Redirect(303, "/account")
}
···
import (
"errors"
+
"fmt"
"strings"
+
"time"
"github.com/bluesky-social/indigo/atproto/syntax"
"github.com/gorilla/sessions"
···
"gorm.io/gorm"
)
+
type OauthSigninInput struct {
+
Username string `form:"username"`
+
Password string `form:"password"`
+
AuthFactorToken string `form:"token"`
+
QueryParams string `form:"query_params"`
}
func (s *Server) getSessionRepoOrErr(e echo.Context) (*models.RepoActor, *sessions.Session, error) {
+
ctx := e.Request().Context()
+
sess, err := session.Get("session", e)
if err != nil {
return nil, nil, err
···
return nil, sess, errors.New("did was not set in session")
}
+
repo, err := s.getRepoActorByDid(ctx, did)
if err != nil {
return nil, sess, err
}
···
func getFlashesFromSession(e echo.Context, sess *sessions.Session) map[string]any {
defer sess.Save(e.Request(), e.Response())
return map[string]any{
+
"errors": sess.Flashes("error"),
+
"successes": sess.Flashes("success"),
+
"tokenrequired": sess.Flashes("tokenrequired"),
}
}
···
}
func (s *Server) handleAccountSigninPost(e echo.Context) error {
+
ctx := e.Request().Context()
+
logger := s.logger.With("name", "handleAccountSigninPost")
+
+
var req OauthSigninInput
if err := e.Bind(&req); err != nil {
+
logger.Error("error binding sign in req", "error", err)
return helpers.ServerError(e, nil)
}
···
idtype = "handle"
} else {
idtype = "email"
+
}
+
+
queryParams := ""
+
if req.QueryParams != "" {
+
queryParams = fmt.Sprintf("?%s", req.QueryParams)
}
// TODO: we should make this a helper since we do it for the base create_session as well
···
var err error
switch idtype {
case "did":
+
err = s.db.Raw(ctx, "SELECT r.*, a.* FROM repos r LEFT JOIN actors a ON r.did = a.did WHERE r.did = ?", nil, req.Username).Scan(&repo).Error
case "handle":
+
err = s.db.Raw(ctx, "SELECT r.*, a.* FROM actors a LEFT JOIN repos r ON a.did = r.did WHERE a.handle = ?", nil, req.Username).Scan(&repo).Error
case "email":
+
err = s.db.Raw(ctx, "SELECT r.*, a.* FROM repos r LEFT JOIN actors a ON r.did = a.did WHERE r.email = ?", nil, req.Username).Scan(&repo).Error
}
if err != nil {
if err == gorm.ErrRecordNotFound {
···
sess.AddFlash("Something went wrong!", "error")
}
sess.Save(e.Request(), e.Response())
+
return e.Redirect(303, "/account/signin"+queryParams)
}
if err := bcrypt.CompareHashAndPassword([]byte(repo.Password), []byte(req.Password)); err != nil {
···
sess.AddFlash("Something went wrong!", "error")
}
sess.Save(e.Request(), e.Response())
+
return e.Redirect(303, "/account/signin"+queryParams)
+
}
+
+
// if repo requires 2FA token and one hasn't been provided, return error prompting for one
+
if repo.TwoFactorType != models.TwoFactorTypeNone && req.AuthFactorToken == "" {
+
err = s.createAndSendTwoFactorCode(ctx, repo)
+
if err != nil {
+
sess.AddFlash("Something went wrong!", "error")
+
sess.Save(e.Request(), e.Response())
+
return e.Redirect(303, "/account/signin"+queryParams)
+
}
+
+
sess.AddFlash("requires 2FA token", "tokenrequired")
+
sess.Save(e.Request(), e.Response())
+
return e.Redirect(303, "/account/signin"+queryParams)
+
}
+
+
// if 2FAis required, now check that the one provided is valid
+
if repo.TwoFactorType != models.TwoFactorTypeNone {
+
if repo.TwoFactorCode == nil || repo.TwoFactorCodeExpiresAt == nil {
+
err = s.createAndSendTwoFactorCode(ctx, repo)
+
if err != nil {
+
sess.AddFlash("Something went wrong!", "error")
+
sess.Save(e.Request(), e.Response())
+
return e.Redirect(303, "/account/signin"+queryParams)
+
}
+
+
sess.AddFlash("requires 2FA token", "tokenrequired")
+
sess.Save(e.Request(), e.Response())
+
return e.Redirect(303, "/account/signin"+queryParams)
+
}
+
+
if *repo.TwoFactorCode != req.AuthFactorToken {
+
return helpers.InvalidTokenError(e)
+
}
+
+
if time.Now().UTC().After(*repo.TwoFactorCodeExpiresAt) {
+
return helpers.ExpiredTokenError(e)
+
}
}
sess.Options = &sessions.Options{
···
return err
}
+
if queryParams != "" {
+
return e.Redirect(303, "/oauth/authorize"+queryParams)
} else {
return e.Redirect(303, "/account")
}
+3 -1
server/handle_actor_put_preferences.go
···
// This is kinda lame. Not great to implement app.bsky in the pds, but alas
func (s *Server) handleActorPutPreferences(e echo.Context) error {
repo := e.Get("repo").(*models.RepoActor)
var prefs map[string]any
···
return err
}
-
if err := s.db.Exec("UPDATE repos SET preferences = ? WHERE did = ?", nil, b, repo.Repo.Did).Error; err != nil {
return err
}
···
// This is kinda lame. Not great to implement app.bsky in the pds, but alas
func (s *Server) handleActorPutPreferences(e echo.Context) error {
+
ctx := e.Request().Context()
+
repo := e.Get("repo").(*models.RepoActor)
var prefs map[string]any
···
return err
}
+
if err := s.db.Exec(ctx, "UPDATE repos SET preferences = ? WHERE did = ?", nil, b, repo.Repo.Did).Error; err != nil {
return err
}
+6 -3
server/handle_identity_request_plc_operation.go
···
)
func (s *Server) handleIdentityRequestPlcOperationSignature(e echo.Context) error {
urepo := e.Get("repo").(*models.RepoActor)
code := fmt.Sprintf("%s-%s", helpers.RandomVarchar(5), helpers.RandomVarchar(5))
eat := time.Now().Add(10 * time.Minute).UTC()
-
if err := s.db.Exec("UPDATE repos SET plc_operation_code = ?, plc_operation_code_expires_at = ? WHERE did = ?", nil, code, eat, urepo.Repo.Did).Error; err != nil {
-
s.logger.Error("error updating user", "error", err)
return helpers.ServerError(e, nil)
}
if err := s.sendPlcTokenReset(urepo.Email, urepo.Handle, code); err != nil {
-
s.logger.Error("error sending mail", "error", err)
return helpers.ServerError(e, nil)
}
···
)
func (s *Server) handleIdentityRequestPlcOperationSignature(e echo.Context) error {
+
ctx := e.Request().Context()
+
logger := s.logger.With("name", "handleIdentityRequestPlcOperationSignature")
+
urepo := e.Get("repo").(*models.RepoActor)
code := fmt.Sprintf("%s-%s", helpers.RandomVarchar(5), helpers.RandomVarchar(5))
eat := time.Now().Add(10 * time.Minute).UTC()
+
if err := s.db.Exec(ctx, "UPDATE repos SET plc_operation_code = ?, plc_operation_code_expires_at = ? WHERE did = ?", nil, code, eat, urepo.Repo.Did).Error; err != nil {
+
logger.Error("error updating user", "error", err)
return helpers.ServerError(e, nil)
}
if err := s.sendPlcTokenReset(urepo.Email, urepo.Handle, code); err != nil {
+
logger.Error("error sending mail", "error", err)
return helpers.ServerError(e, nil)
}
+8 -6
server/handle_identity_sign_plc_operation.go
···
}
func (s *Server) handleSignPlcOperation(e echo.Context) error {
repo := e.Get("repo").(*models.RepoActor)
var req ComAtprotoSignPlcOperationRequest
if err := e.Bind(&req); err != nil {
-
s.logger.Error("error binding", "error", err)
return helpers.ServerError(e, nil)
}
···
ctx := context.WithValue(e.Request().Context(), "skip-cache", true)
log, err := identity.FetchDidAuditLog(ctx, nil, repo.Repo.Did)
if err != nil {
-
s.logger.Error("error fetching doc", "error", err)
return helpers.ServerError(e, nil)
}
···
k, err := atcrypto.ParsePrivateBytesK256(repo.SigningKey)
if err != nil {
-
s.logger.Error("error parsing signing key", "error", err)
return helpers.ServerError(e, nil)
}
if err := s.plcClient.SignOp(k, &op); err != nil {
-
s.logger.Error("error signing plc operation", "error", err)
return helpers.ServerError(e, nil)
}
-
if err := s.db.Exec("UPDATE repos SET plc_operation_code = NULL, plc_operation_code_expires_at = NULL WHERE did = ?", nil, repo.Repo.Did).Error; err != nil {
-
s.logger.Error("error updating repo", "error", err)
return helpers.ServerError(e, nil)
}
···
}
func (s *Server) handleSignPlcOperation(e echo.Context) error {
+
logger := s.logger.With("name", "handleSignPlcOperation")
+
repo := e.Get("repo").(*models.RepoActor)
var req ComAtprotoSignPlcOperationRequest
if err := e.Bind(&req); err != nil {
+
logger.Error("error binding", "error", err)
return helpers.ServerError(e, nil)
}
···
ctx := context.WithValue(e.Request().Context(), "skip-cache", true)
log, err := identity.FetchDidAuditLog(ctx, nil, repo.Repo.Did)
if err != nil {
+
logger.Error("error fetching doc", "error", err)
return helpers.ServerError(e, nil)
}
···
k, err := atcrypto.ParsePrivateBytesK256(repo.SigningKey)
if err != nil {
+
logger.Error("error parsing signing key", "error", err)
return helpers.ServerError(e, nil)
}
if err := s.plcClient.SignOp(k, &op); err != nil {
+
logger.Error("error signing plc operation", "error", err)
return helpers.ServerError(e, nil)
}
+
if err := s.db.Exec(ctx, "UPDATE repos SET plc_operation_code = NULL, plc_operation_code_expires_at = NULL WHERE did = ?", nil, repo.Repo.Did).Error; err != nil {
+
logger.Error("error updating repo", "error", err)
return helpers.ServerError(e, nil)
}
+6 -4
server/handle_identity_submit_plc_operation.go
···
}
func (s *Server) handleSubmitPlcOperation(e echo.Context) error {
repo := e.Get("repo").(*models.RepoActor)
var req ComAtprotoSubmitPlcOperationRequest
if err := e.Bind(&req); err != nil {
-
s.logger.Error("error binding", "error", err)
return helpers.ServerError(e, nil)
}
···
k, err := atcrypto.ParsePrivateBytesK256(repo.SigningKey)
if err != nil {
-
s.logger.Error("error parsing key", "error", err)
return helpers.ServerError(e, nil)
}
required, err := s.plcClient.CreateDidCredentials(k, "", repo.Actor.Handle)
if err != nil {
-
s.logger.Error("error crating did credentials", "error", err)
return helpers.ServerError(e, nil)
}
···
}
if err := s.passport.BustDoc(context.TODO(), repo.Repo.Did); err != nil {
-
s.logger.Warn("error busting did doc", "error", err)
}
s.evtman.AddEvent(context.TODO(), &events.XRPCStreamEvent{
···
}
func (s *Server) handleSubmitPlcOperation(e echo.Context) error {
+
logger := s.logger.With("name", "handleIdentitySubmitPlcOperation")
+
repo := e.Get("repo").(*models.RepoActor)
var req ComAtprotoSubmitPlcOperationRequest
if err := e.Bind(&req); err != nil {
+
logger.Error("error binding", "error", err)
return helpers.ServerError(e, nil)
}
···
k, err := atcrypto.ParsePrivateBytesK256(repo.SigningKey)
if err != nil {
+
logger.Error("error parsing key", "error", err)
return helpers.ServerError(e, nil)
}
required, err := s.plcClient.CreateDidCredentials(k, "", repo.Actor.Handle)
if err != nil {
+
logger.Error("error crating did credentials", "error", err)
return helpers.ServerError(e, nil)
}
···
}
if err := s.passport.BustDoc(context.TODO(), repo.Repo.Did); err != nil {
+
logger.Warn("error busting did doc", "error", err)
}
s.evtman.AddEvent(context.TODO(), &events.XRPCStreamEvent{
+8 -6
server/handle_identity_update_handle.go
···
}
func (s *Server) handleIdentityUpdateHandle(e echo.Context) error {
repo := e.Get("repo").(*models.RepoActor)
var req ComAtprotoIdentityUpdateHandleRequest
if err := e.Bind(&req); err != nil {
-
s.logger.Error("error binding", "error", err)
return helpers.ServerError(e, nil)
}
···
if strings.HasPrefix(repo.Repo.Did, "did:plc:") {
log, err := identity.FetchDidAuditLog(ctx, nil, repo.Repo.Did)
if err != nil {
-
s.logger.Error("error fetching doc", "error", err)
return helpers.ServerError(e, nil)
}
···
k, err := atcrypto.ParsePrivateBytesK256(repo.SigningKey)
if err != nil {
-
s.logger.Error("error parsing signing key", "error", err)
return helpers.ServerError(e, nil)
}
···
}
if err := s.passport.BustDoc(context.TODO(), repo.Repo.Did); err != nil {
-
s.logger.Warn("error busting did doc", "error", err)
}
s.evtman.AddEvent(context.TODO(), &events.XRPCStreamEvent{
···
},
})
-
if err := s.db.Exec("UPDATE actors SET handle = ? WHERE did = ?", nil, req.Handle, repo.Repo.Did).Error; err != nil {
-
s.logger.Error("error updating handle in db", "error", err)
return helpers.ServerError(e, nil)
}
···
}
func (s *Server) handleIdentityUpdateHandle(e echo.Context) error {
+
logger := s.logger.With("name", "handleIdentityUpdateHandle")
+
repo := e.Get("repo").(*models.RepoActor)
var req ComAtprotoIdentityUpdateHandleRequest
if err := e.Bind(&req); err != nil {
+
logger.Error("error binding", "error", err)
return helpers.ServerError(e, nil)
}
···
if strings.HasPrefix(repo.Repo.Did, "did:plc:") {
log, err := identity.FetchDidAuditLog(ctx, nil, repo.Repo.Did)
if err != nil {
+
logger.Error("error fetching doc", "error", err)
return helpers.ServerError(e, nil)
}
···
k, err := atcrypto.ParsePrivateBytesK256(repo.SigningKey)
if err != nil {
+
logger.Error("error parsing signing key", "error", err)
return helpers.ServerError(e, nil)
}
···
}
if err := s.passport.BustDoc(context.TODO(), repo.Repo.Did); err != nil {
+
logger.Warn("error busting did doc", "error", err)
}
s.evtman.AddEvent(context.TODO(), &events.XRPCStreamEvent{
···
},
})
+
if err := s.db.Exec(ctx, "UPDATE actors SET handle = ? WHERE did = ?", nil, req.Handle, repo.Repo.Did).Error; err != nil {
+
logger.Error("error updating handle in db", "error", err)
return helpers.ServerError(e, nil)
}
+14 -11
server/handle_import_repo.go
···
)
func (s *Server) handleRepoImportRepo(e echo.Context) error {
urepo := e.Get("repo").(*models.RepoActor)
b, err := io.ReadAll(e.Request().Body)
if err != nil {
-
s.logger.Error("could not read bytes in import request", "error", err)
return helpers.ServerError(e, nil)
}
···
cs, err := car.NewCarReader(bytes.NewReader(b))
if err != nil {
-
s.logger.Error("could not read car in import request", "error", err)
return helpers.ServerError(e, nil)
}
orderedBlocks := []blocks.Block{}
currBlock, err := cs.Next()
if err != nil {
-
s.logger.Error("could not get first block from car", "error", err)
return helpers.ServerError(e, nil)
}
currBlockCt := 1
for currBlock != nil {
-
s.logger.Info("someone is importing their repo", "block", currBlockCt)
orderedBlocks = append(orderedBlocks, currBlock)
next, _ := cs.Next()
currBlock = next
···
slices.Reverse(orderedBlocks)
if err := bs.PutMany(context.TODO(), orderedBlocks); err != nil {
-
s.logger.Error("could not insert blocks", "error", err)
return helpers.ServerError(e, nil)
}
r, err := repo.OpenRepo(context.TODO(), bs, cs.Header.Roots[0])
if err != nil {
-
s.logger.Error("could not open repo", "error", err)
return helpers.ServerError(e, nil)
}
-
tx := s.db.BeginDangerously()
clock := syntax.NewTIDClock(0)
···
cidStr := cid.String()
b, err := bs.Get(context.TODO(), cid)
if err != nil {
-
s.logger.Error("record bytes don't exist in blockstore", "error", err)
return helpers.ServerError(e, nil)
}
···
return nil
}); err != nil {
tx.Rollback()
-
s.logger.Error("record bytes don't exist in blockstore", "error", err)
return helpers.ServerError(e, nil)
}
···
root, rev, err := r.Commit(context.TODO(), urepo.SignFor)
if err != nil {
-
s.logger.Error("error committing", "error", err)
return helpers.ServerError(e, nil)
}
if err := s.UpdateRepo(context.TODO(), urepo.Repo.Did, root, rev); err != nil {
-
s.logger.Error("error updating repo after commit", "error", err)
return helpers.ServerError(e, nil)
}
···
)
func (s *Server) handleRepoImportRepo(e echo.Context) error {
+
ctx := e.Request().Context()
+
logger := s.logger.With("name", "handleImportRepo")
+
urepo := e.Get("repo").(*models.RepoActor)
b, err := io.ReadAll(e.Request().Body)
if err != nil {
+
logger.Error("could not read bytes in import request", "error", err)
return helpers.ServerError(e, nil)
}
···
cs, err := car.NewCarReader(bytes.NewReader(b))
if err != nil {
+
logger.Error("could not read car in import request", "error", err)
return helpers.ServerError(e, nil)
}
orderedBlocks := []blocks.Block{}
currBlock, err := cs.Next()
if err != nil {
+
logger.Error("could not get first block from car", "error", err)
return helpers.ServerError(e, nil)
}
currBlockCt := 1
for currBlock != nil {
+
logger.Info("someone is importing their repo", "block", currBlockCt)
orderedBlocks = append(orderedBlocks, currBlock)
next, _ := cs.Next()
currBlock = next
···
slices.Reverse(orderedBlocks)
if err := bs.PutMany(context.TODO(), orderedBlocks); err != nil {
+
logger.Error("could not insert blocks", "error", err)
return helpers.ServerError(e, nil)
}
r, err := repo.OpenRepo(context.TODO(), bs, cs.Header.Roots[0])
if err != nil {
+
logger.Error("could not open repo", "error", err)
return helpers.ServerError(e, nil)
}
+
tx := s.db.BeginDangerously(ctx)
clock := syntax.NewTIDClock(0)
···
cidStr := cid.String()
b, err := bs.Get(context.TODO(), cid)
if err != nil {
+
logger.Error("record bytes don't exist in blockstore", "error", err)
return helpers.ServerError(e, nil)
}
···
return nil
}); err != nil {
tx.Rollback()
+
logger.Error("record bytes don't exist in blockstore", "error", err)
return helpers.ServerError(e, nil)
}
···
root, rev, err := r.Commit(context.TODO(), urepo.SignFor)
if err != nil {
+
logger.Error("error committing", "error", err)
return helpers.ServerError(e, nil)
}
if err := s.UpdateRepo(context.TODO(), urepo.Repo.Did, root, rev); err != nil {
+
logger.Error("error updating repo after commit", "error", err)
return helpers.ServerError(e, nil)
}
+10 -5
server/handle_oauth_authorize.go
···
)
func (s *Server) handleOauthAuthorizeGet(e echo.Context) error {
reqUri := e.QueryParam("request_uri")
if reqUri == "" {
// render page for logged out dev
···
}
var req provider.OauthAuthorizationRequest
-
if err := s.db.Raw("SELECT * FROM oauth_authorization_requests WHERE request_id = ?", nil, reqId).Scan(&req).Error; err != nil {
return helpers.ServerError(e, to.StringPtr(err.Error()))
}
···
}
func (s *Server) handleOauthAuthorizePost(e echo.Context) error {
repo, _, err := s.getSessionRepoOrErr(e)
if err != nil {
return e.Redirect(303, "/account/signin")
···
var req OauthAuthorizePostRequest
if err := e.Bind(&req); err != nil {
-
s.logger.Error("error binding authorize post request", "error", err)
return helpers.InputError(e, nil)
}
···
}
var authReq provider.OauthAuthorizationRequest
-
if err := s.db.Raw("SELECT * FROM oauth_authorization_requests WHERE request_id = ?", nil, reqId).Scan(&authReq).Error; err != nil {
return helpers.ServerError(e, to.StringPtr(err.Error()))
}
···
code := oauth.GenerateCode()
-
if err := s.db.Exec("UPDATE oauth_authorization_requests SET sub = ?, code = ?, accepted = ?, ip = ? WHERE request_id = ?", nil, repo.Repo.Did, code, true, e.RealIP(), reqId).Error; err != nil {
-
s.logger.Error("error updating authorization request", "error", err)
return helpers.ServerError(e, nil)
}
···
)
func (s *Server) handleOauthAuthorizeGet(e echo.Context) error {
+
ctx := e.Request().Context()
+
reqUri := e.QueryParam("request_uri")
if reqUri == "" {
// render page for logged out dev
···
}
var req provider.OauthAuthorizationRequest
+
if err := s.db.Raw(ctx, "SELECT * FROM oauth_authorization_requests WHERE request_id = ?", nil, reqId).Scan(&req).Error; err != nil {
return helpers.ServerError(e, to.StringPtr(err.Error()))
}
···
}
func (s *Server) handleOauthAuthorizePost(e echo.Context) error {
+
ctx := e.Request().Context()
+
logger := s.logger.With("name", "handleOauthAuthorizePost")
+
repo, _, err := s.getSessionRepoOrErr(e)
if err != nil {
return e.Redirect(303, "/account/signin")
···
var req OauthAuthorizePostRequest
if err := e.Bind(&req); err != nil {
+
logger.Error("error binding authorize post request", "error", err)
return helpers.InputError(e, nil)
}
···
}
var authReq provider.OauthAuthorizationRequest
+
if err := s.db.Raw(ctx, "SELECT * FROM oauth_authorization_requests WHERE request_id = ?", nil, reqId).Scan(&authReq).Error; err != nil {
return helpers.ServerError(e, to.StringPtr(err.Error()))
}
···
code := oauth.GenerateCode()
+
if err := s.db.Exec(ctx, "UPDATE oauth_authorization_requests SET sub = ?, code = ?, accepted = ?, ip = ? WHERE request_id = ?", nil, repo.Repo.Did, code, true, e.RealIP(), reqId).Error; err != nil {
+
logger.Error("error updating authorization request", "error", err)
return helpers.ServerError(e, nil)
}
+11 -8
server/handle_oauth_par.go
···
}
func (s *Server) handleOauthPar(e echo.Context) error {
var parRequest provider.ParRequest
if err := e.Bind(&parRequest); err != nil {
-
s.logger.Error("error binding for par request", "error", err)
return helpers.ServerError(e, nil)
}
if err := e.Validate(parRequest); err != nil {
-
s.logger.Error("missing parameters for par request", "error", err)
return helpers.InputError(e, nil)
}
···
"error": "use_dpop_nonce",
})
}
-
s.logger.Error("error getting dpop proof", "error", err)
return helpers.InputError(e, nil)
}
···
AllowMissingDpopProof: true,
})
if err != nil {
-
s.logger.Error("error authenticating client", "client_id", parRequest.ClientID, "error", err)
return helpers.InputError(e, to.StringPtr(err.Error()))
}
···
} else {
if !client.Metadata.DpopBoundAccessTokens {
msg := "dpop bound access tokens are not enabled for this client"
-
s.logger.Error(msg)
return helpers.InputError(e, &msg)
}
if dpopProof.JKT != *parRequest.DpopJkt {
msg := "supplied dpop jkt does not match header dpop jkt"
-
s.logger.Error(msg)
return helpers.InputError(e, &msg)
}
}
···
ExpiresAt: eat,
}
-
if err := s.db.Create(authRequest, nil).Error; err != nil {
-
s.logger.Error("error creating auth request in db", "error", err)
return helpers.ServerError(e, nil)
}
···
}
func (s *Server) handleOauthPar(e echo.Context) error {
+
ctx := e.Request().Context()
+
logger := s.logger.With("name", "handleOauthPar")
+
var parRequest provider.ParRequest
if err := e.Bind(&parRequest); err != nil {
+
logger.Error("error binding for par request", "error", err)
return helpers.ServerError(e, nil)
}
if err := e.Validate(parRequest); err != nil {
+
logger.Error("missing parameters for par request", "error", err)
return helpers.InputError(e, nil)
}
···
"error": "use_dpop_nonce",
})
}
+
logger.Error("error getting dpop proof", "error", err)
return helpers.InputError(e, nil)
}
···
AllowMissingDpopProof: true,
})
if err != nil {
+
logger.Error("error authenticating client", "client_id", parRequest.ClientID, "error", err)
return helpers.InputError(e, to.StringPtr(err.Error()))
}
···
} else {
if !client.Metadata.DpopBoundAccessTokens {
msg := "dpop bound access tokens are not enabled for this client"
+
logger.Error(msg)
return helpers.InputError(e, &msg)
}
if dpopProof.JKT != *parRequest.DpopJkt {
msg := "supplied dpop jkt does not match header dpop jkt"
+
logger.Error(msg)
return helpers.InputError(e, &msg)
}
}
···
ExpiresAt: eat,
}
+
if err := s.db.Create(ctx, authRequest, nil).Error; err != nil {
+
logger.Error("error creating auth request in db", "error", err)
return helpers.ServerError(e, nil)
}
+16 -13
server/handle_oauth_token.go
···
}
func (s *Server) handleOauthToken(e echo.Context) error {
var req OauthTokenRequest
if err := e.Bind(&req); err != nil {
-
s.logger.Error("error binding token request", "error", err)
return helpers.ServerError(e, nil)
}
···
"error": "use_dpop_nonce",
})
}
-
s.logger.Error("error getting dpop proof", "error", err)
return helpers.InputError(e, nil)
}
···
AllowMissingDpopProof: true,
})
if err != nil {
-
s.logger.Error("error authenticating client", "client_id", req.ClientID, "error", err)
return helpers.InputError(e, to.StringPtr(err.Error()))
}
···
var authReq provider.OauthAuthorizationRequest
// get the lil guy and delete him
-
if err := s.db.Raw("DELETE FROM oauth_authorization_requests WHERE code = ? RETURNING *", nil, *req.Code).Scan(&authReq).Error; err != nil {
-
s.logger.Error("error finding authorization request", "error", err)
return helpers.ServerError(e, nil)
}
···
case "S256":
inputChal, err := base64.RawURLEncoding.DecodeString(*authReq.Parameters.CodeChallenge)
if err != nil {
-
s.logger.Error("error decoding code challenge", "error", err)
return helpers.ServerError(e, nil)
}
···
return helpers.InputError(e, to.StringPtr("code_challenge parameter wasn't provided"))
}
-
repo, err := s.getRepoActorByDid(*authReq.Sub)
if err != nil {
helpers.InputError(e, to.StringPtr("unable to find actor"))
}
···
return err
}
-
if err := s.db.Create(&provider.OauthToken{
ClientId: authReq.ClientId,
ClientAuth: *clientAuth,
Parameters: authReq.Parameters,
···
RefreshToken: refreshToken,
Ip: authReq.Ip,
}, nil).Error; err != nil {
-
s.logger.Error("error creating token in db", "error", err)
return helpers.ServerError(e, nil)
}
···
}
var oauthToken provider.OauthToken
-
if err := s.db.Raw("SELECT * FROM oauth_tokens WHERE refresh_token = ?", nil, req.RefreshToken).Scan(&oauthToken).Error; err != nil {
-
s.logger.Error("error finding oauth token by refresh token", "error", err, "refresh_token", req.RefreshToken)
return helpers.ServerError(e, nil)
}
···
return err
}
-
if err := s.db.Exec("UPDATE oauth_tokens SET token = ?, refresh_token = ?, expires_at = ?, updated_at = ? WHERE refresh_token = ?", nil, accessString, nextRefreshToken, eat, now, *req.RefreshToken).Error; err != nil {
-
s.logger.Error("error updating token", "error", err)
return helpers.ServerError(e, nil)
}
···
}
func (s *Server) handleOauthToken(e echo.Context) error {
+
ctx := e.Request().Context()
+
logger := s.logger.With("name", "handleOauthToken")
+
var req OauthTokenRequest
if err := e.Bind(&req); err != nil {
+
logger.Error("error binding token request", "error", err)
return helpers.ServerError(e, nil)
}
···
"error": "use_dpop_nonce",
})
}
+
logger.Error("error getting dpop proof", "error", err)
return helpers.InputError(e, nil)
}
···
AllowMissingDpopProof: true,
})
if err != nil {
+
logger.Error("error authenticating client", "client_id", req.ClientID, "error", err)
return helpers.InputError(e, to.StringPtr(err.Error()))
}
···
var authReq provider.OauthAuthorizationRequest
// get the lil guy and delete him
+
if err := s.db.Raw(ctx, "DELETE FROM oauth_authorization_requests WHERE code = ? RETURNING *", nil, *req.Code).Scan(&authReq).Error; err != nil {
+
logger.Error("error finding authorization request", "error", err)
return helpers.ServerError(e, nil)
}
···
case "S256":
inputChal, err := base64.RawURLEncoding.DecodeString(*authReq.Parameters.CodeChallenge)
if err != nil {
+
logger.Error("error decoding code challenge", "error", err)
return helpers.ServerError(e, nil)
}
···
return helpers.InputError(e, to.StringPtr("code_challenge parameter wasn't provided"))
}
+
repo, err := s.getRepoActorByDid(ctx, *authReq.Sub)
if err != nil {
helpers.InputError(e, to.StringPtr("unable to find actor"))
}
···
return err
}
+
if err := s.db.Create(ctx, &provider.OauthToken{
ClientId: authReq.ClientId,
ClientAuth: *clientAuth,
Parameters: authReq.Parameters,
···
RefreshToken: refreshToken,
Ip: authReq.Ip,
}, nil).Error; err != nil {
+
logger.Error("error creating token in db", "error", err)
return helpers.ServerError(e, nil)
}
···
}
var oauthToken provider.OauthToken
+
if err := s.db.Raw(ctx, "SELECT * FROM oauth_tokens WHERE refresh_token = ?", nil, req.RefreshToken).Scan(&oauthToken).Error; err != nil {
+
logger.Error("error finding oauth token by refresh token", "error", err, "refresh_token", req.RefreshToken)
return helpers.ServerError(e, nil)
}
···
return err
}
+
if err := s.db.Exec(ctx, "UPDATE oauth_tokens SET token = ?, refresh_token = ?, expires_at = ?, updated_at = ? WHERE refresh_token = ?", nil, accessString, nextRefreshToken, eat, now, *req.RefreshToken).Error; err != nil {
+
logger.Error("error updating token", "error", err)
return helpers.ServerError(e, nil)
}
+6 -6
server/handle_proxy.go
···
}
func (s *Server) handleProxy(e echo.Context) error {
-
lgr := s.logger.With("handler", "handleProxy")
repo, isAuthed := e.Get("repo").(*models.RepoActor)
···
endpoint, svcDid, err := s.getAtprotoProxyEndpointFromRequest(e)
if err != nil {
-
lgr.Error("could not get atproto proxy", "error", err)
return helpers.ServerError(e, nil)
}
···
}
hj, err := json.Marshal(header)
if err != nil {
-
lgr.Error("error marshaling header", "error", err)
return helpers.ServerError(e, nil)
}
···
}
pj, err := json.Marshal(payload)
if err != nil {
-
lgr.Error("error marashaling payload", "error", err)
return helpers.ServerError(e, nil)
}
···
sk, err := secp256k1secec.NewPrivateKey(repo.SigningKey)
if err != nil {
-
lgr.Error("can't load private key", "error", err)
return err
}
R, S, _, err := sk.SignRaw(rand.Reader, hash[:])
if err != nil {
-
lgr.Error("error signing", "error", err)
}
rBytes := R.Bytes()
···
}
func (s *Server) handleProxy(e echo.Context) error {
+
logger := s.logger.With("handler", "handleProxy")
repo, isAuthed := e.Get("repo").(*models.RepoActor)
···
endpoint, svcDid, err := s.getAtprotoProxyEndpointFromRequest(e)
if err != nil {
+
logger.Error("could not get atproto proxy", "error", err)
return helpers.ServerError(e, nil)
}
···
}
hj, err := json.Marshal(header)
if err != nil {
+
logger.Error("error marshaling header", "error", err)
return helpers.ServerError(e, nil)
}
···
}
pj, err := json.Marshal(payload)
if err != nil {
+
logger.Error("error marashaling payload", "error", err)
return helpers.ServerError(e, nil)
}
···
sk, err := secp256k1secec.NewPrivateKey(repo.SigningKey)
if err != nil {
+
logger.Error("can't load private key", "error", err)
return err
}
R, S, _, err := sk.SignRaw(rand.Reader, hash[:])
if err != nil {
+
logger.Error("error signing", "error", err)
}
rBytes := R.Bytes()
+14 -11
server/handle_repo_apply_writes.go
···
"github.com/labstack/echo/v4"
)
-
type ComAtprotoRepoApplyWritesRequest struct {
Repo string `json:"repo" validate:"required,atproto-did"`
Validate *bool `json:"bool,omitempty"`
Writes []ComAtprotoRepoApplyWritesItem `json:"writes"`
···
Value *MarshalableMap `json:"value,omitempty"`
}
-
type ComAtprotoRepoApplyWritesResponse struct {
Commit RepoCommit `json:"commit"`
Results []ApplyWriteResult `json:"results"`
}
func (s *Server) handleApplyWrites(e echo.Context) error {
-
repo := e.Get("repo").(*models.RepoActor)
-
var req ComAtprotoRepoApplyWritesRequest
if err := e.Bind(&req); err != nil {
-
s.logger.Error("error binding", "error", err)
return helpers.ServerError(e, nil)
}
if err := e.Validate(req); err != nil {
-
s.logger.Error("error validating", "error", err)
return helpers.InputError(e, nil)
}
if repo.Repo.Did != req.Repo {
-
s.logger.Warn("mismatched repo/auth")
return helpers.InputError(e, nil)
}
-
ops := []Op{}
for _, item := range req.Writes {
ops = append(ops, Op{
Type: OpType(item.Type),
···
})
}
-
results, err := s.repoman.applyWrites(repo.Repo, ops, req.SwapCommit)
if err != nil {
-
s.logger.Error("error applying writes", "error", err)
return helpers.ServerError(e, nil)
}
···
results[i].Commit = nil
}
-
return e.JSON(200, ComAtprotoRepoApplyWritesResponse{
Commit: commit,
Results: results,
})
···
"github.com/labstack/echo/v4"
)
+
type ComAtprotoRepoApplyWritesInput struct {
Repo string `json:"repo" validate:"required,atproto-did"`
Validate *bool `json:"bool,omitempty"`
Writes []ComAtprotoRepoApplyWritesItem `json:"writes"`
···
Value *MarshalableMap `json:"value,omitempty"`
}
+
type ComAtprotoRepoApplyWritesOutput struct {
Commit RepoCommit `json:"commit"`
Results []ApplyWriteResult `json:"results"`
}
func (s *Server) handleApplyWrites(e echo.Context) error {
+
ctx := e.Request().Context()
+
logger := s.logger.With("name", "handleRepoApplyWrites")
+
var req ComAtprotoRepoApplyWritesInput
if err := e.Bind(&req); err != nil {
+
logger.Error("error binding", "error", err)
return helpers.ServerError(e, nil)
}
if err := e.Validate(req); err != nil {
+
logger.Error("error validating", "error", err)
return helpers.InputError(e, nil)
}
+
repo := e.Get("repo").(*models.RepoActor)
+
if repo.Repo.Did != req.Repo {
+
logger.Warn("mismatched repo/auth")
return helpers.InputError(e, nil)
}
+
ops := make([]Op, 0, len(req.Writes))
for _, item := range req.Writes {
ops = append(ops, Op{
Type: OpType(item.Type),
···
})
}
+
results, err := s.repoman.applyWrites(ctx, repo.Repo, ops, req.SwapCommit)
if err != nil {
+
logger.Error("error applying writes", "error", err)
return helpers.ServerError(e, nil)
}
···
results[i].Commit = nil
}
+
return e.JSON(200, ComAtprotoRepoApplyWritesOutput{
Commit: commit,
Results: results,
})
+10 -7
server/handle_repo_create_record.go
···
"github.com/labstack/echo/v4"
)
-
type ComAtprotoRepoCreateRecordRequest struct {
Repo string `json:"repo" validate:"required,atproto-did"`
Collection string `json:"collection" validate:"required,atproto-nsid"`
Rkey *string `json:"rkey,omitempty"`
···
}
func (s *Server) handleCreateRecord(e echo.Context) error {
repo := e.Get("repo").(*models.RepoActor)
-
var req ComAtprotoRepoCreateRecordRequest
if err := e.Bind(&req); err != nil {
-
s.logger.Error("error binding", "error", err)
return helpers.ServerError(e, nil)
}
if err := e.Validate(req); err != nil {
-
s.logger.Error("error validating", "error", err)
return helpers.InputError(e, nil)
}
if repo.Repo.Did != req.Repo {
-
s.logger.Warn("mismatched repo/auth")
return helpers.InputError(e, nil)
}
···
optype = OpTypeUpdate
}
-
results, err := s.repoman.applyWrites(repo.Repo, []Op{
{
Type: optype,
Collection: req.Collection,
···
},
}, req.SwapCommit)
if err != nil {
-
s.logger.Error("error applying writes", "error", err)
return helpers.ServerError(e, nil)
}
···
"github.com/labstack/echo/v4"
)
+
type ComAtprotoRepoCreateRecordInput struct {
Repo string `json:"repo" validate:"required,atproto-did"`
Collection string `json:"collection" validate:"required,atproto-nsid"`
Rkey *string `json:"rkey,omitempty"`
···
}
func (s *Server) handleCreateRecord(e echo.Context) error {
+
ctx := e.Request().Context()
+
logger := s.logger.With("name", "handleCreateRecord")
+
repo := e.Get("repo").(*models.RepoActor)
+
var req ComAtprotoRepoCreateRecordInput
if err := e.Bind(&req); err != nil {
+
logger.Error("error binding", "error", err)
return helpers.ServerError(e, nil)
}
if err := e.Validate(req); err != nil {
+
logger.Error("error validating", "error", err)
return helpers.InputError(e, nil)
}
if repo.Repo.Did != req.Repo {
+
logger.Warn("mismatched repo/auth")
return helpers.InputError(e, nil)
}
···
optype = OpTypeUpdate
}
+
results, err := s.repoman.applyWrites(ctx, repo.Repo, []Op{
{
Type: optype,
Collection: req.Collection,
···
},
}, req.SwapCommit)
if err != nil {
+
logger.Error("error applying writes", "error", err)
return helpers.ServerError(e, nil)
}
+10 -7
server/handle_repo_delete_record.go
···
"github.com/labstack/echo/v4"
)
-
type ComAtprotoRepoDeleteRecordRequest struct {
Repo string `json:"repo" validate:"required,atproto-did"`
Collection string `json:"collection" validate:"required,atproto-nsid"`
Rkey string `json:"rkey" validate:"required,atproto-rkey"`
···
}
func (s *Server) handleDeleteRecord(e echo.Context) error {
repo := e.Get("repo").(*models.RepoActor)
-
var req ComAtprotoRepoDeleteRecordRequest
if err := e.Bind(&req); err != nil {
-
s.logger.Error("error binding", "error", err)
return helpers.ServerError(e, nil)
}
if err := e.Validate(req); err != nil {
-
s.logger.Error("error validating", "error", err)
return helpers.InputError(e, nil)
}
if repo.Repo.Did != req.Repo {
-
s.logger.Warn("mismatched repo/auth")
return helpers.InputError(e, nil)
}
-
results, err := s.repoman.applyWrites(repo.Repo, []Op{
{
Type: OpTypeDelete,
Collection: req.Collection,
···
},
}, req.SwapCommit)
if err != nil {
-
s.logger.Error("error applying writes", "error", err)
return helpers.ServerError(e, nil)
}
···
"github.com/labstack/echo/v4"
)
+
type ComAtprotoRepoDeleteRecordInput struct {
Repo string `json:"repo" validate:"required,atproto-did"`
Collection string `json:"collection" validate:"required,atproto-nsid"`
Rkey string `json:"rkey" validate:"required,atproto-rkey"`
···
}
func (s *Server) handleDeleteRecord(e echo.Context) error {
+
ctx := e.Request().Context()
+
logger := s.logger.With("name", "handleDeleteRecord")
+
repo := e.Get("repo").(*models.RepoActor)
+
var req ComAtprotoRepoDeleteRecordInput
if err := e.Bind(&req); err != nil {
+
logger.Error("error binding", "error", err)
return helpers.ServerError(e, nil)
}
if err := e.Validate(req); err != nil {
+
logger.Error("error validating", "error", err)
return helpers.InputError(e, nil)
}
if repo.Repo.Did != req.Repo {
+
logger.Warn("mismatched repo/auth")
return helpers.InputError(e, nil)
}
+
results, err := s.repoman.applyWrites(ctx, repo.Repo, []Op{
{
Type: OpTypeDelete,
Collection: req.Collection,
···
},
}, req.SwapCommit)
if err != nil {
+
logger.Error("error applying writes", "error", err)
return helpers.ServerError(e, nil)
}
+8 -5
server/handle_repo_describe_repo.go
···
}
func (s *Server) handleDescribeRepo(e echo.Context) error {
did := e.QueryParam("repo")
-
repo, err := s.getRepoActorByDid(did)
if err != nil {
if err == gorm.ErrRecordNotFound {
return helpers.InputError(e, to.StringPtr("RepoNotFound"))
}
-
s.logger.Error("error looking up repo", "error", err)
return helpers.ServerError(e, nil)
}
···
diddoc, err := s.passport.FetchDoc(e.Request().Context(), repo.Repo.Did)
if err != nil {
-
s.logger.Error("error fetching diddoc", "error", err)
return helpers.ServerError(e, nil)
}
···
}
var records []models.Record
-
if err := s.db.Raw("SELECT DISTINCT(nsid) FROM records WHERE did = ?", nil, repo.Repo.Did).Scan(&records).Error; err != nil {
-
s.logger.Error("error getting collections", "error", err)
return helpers.ServerError(e, nil)
}
···
}
func (s *Server) handleDescribeRepo(e echo.Context) error {
+
ctx := e.Request().Context()
+
logger := s.logger.With("name", "handleDescribeRepo")
+
did := e.QueryParam("repo")
+
repo, err := s.getRepoActorByDid(ctx, did)
if err != nil {
if err == gorm.ErrRecordNotFound {
return helpers.InputError(e, to.StringPtr("RepoNotFound"))
}
+
logger.Error("error looking up repo", "error", err)
return helpers.ServerError(e, nil)
}
···
diddoc, err := s.passport.FetchDoc(e.Request().Context(), repo.Repo.Did)
if err != nil {
+
logger.Error("error fetching diddoc", "error", err)
return helpers.ServerError(e, nil)
}
···
}
var records []models.Record
+
if err := s.db.Raw(ctx, "SELECT DISTINCT(nsid) FROM records WHERE did = ?", nil, repo.Repo.Did).Scan(&records).Error; err != nil {
+
logger.Error("error getting collections", "error", err)
return helpers.ServerError(e, nil)
}
+3 -1
server/handle_repo_get_record.go
···
}
func (s *Server) handleRepoGetRecord(e echo.Context) error {
repo := e.QueryParam("repo")
collection := e.QueryParam("collection")
rkey := e.QueryParam("rkey")
···
}
var record models.Record
-
if err := s.db.Raw("SELECT * FROM records WHERE did = ? AND nsid = ? AND rkey = ?"+cidquery, nil, params...).Scan(&record).Error; err != nil {
// TODO: handle error nicely
return err
}
···
}
func (s *Server) handleRepoGetRecord(e echo.Context) error {
+
ctx := e.Request().Context()
+
repo := e.QueryParam("repo")
collection := e.QueryParam("collection")
rkey := e.QueryParam("rkey")
···
}
var record models.Record
+
if err := s.db.Raw(ctx, "SELECT * FROM records WHERE did = ? AND nsid = ? AND rkey = ?"+cidquery, nil, params...).Scan(&record).Error; err != nil {
// TODO: handle error nicely
return err
}
+6 -3
server/handle_repo_list_missing_blobs.go
···
}
func (s *Server) handleListMissingBlobs(e echo.Context) error {
urepo := e.Get("repo").(*models.RepoActor)
limitStr := e.QueryParam("limit")
···
}
var records []models.Record
-
if err := s.db.Raw("SELECT * FROM records WHERE did = ?", nil, urepo.Repo.Did).Scan(&records).Error; err != nil {
-
s.logger.Error("failed to get records for listMissingBlobs", "error", err)
return helpers.ServerError(e, nil)
}
···
}
var count int64
-
if err := s.db.Raw("SELECT COUNT(*) FROM blobs WHERE did = ? AND cid = ?", nil, urepo.Repo.Did, ref.cid.Bytes()).Scan(&count).Error; err != nil {
continue
}
···
}
func (s *Server) handleListMissingBlobs(e echo.Context) error {
+
ctx := e.Request().Context()
+
logger := s.logger.With("name", "handleListMissingBlos")
+
urepo := e.Get("repo").(*models.RepoActor)
limitStr := e.QueryParam("limit")
···
}
var records []models.Record
+
if err := s.db.Raw(ctx, "SELECT * FROM records WHERE did = ?", nil, urepo.Repo.Did).Scan(&records).Error; err != nil {
+
logger.Error("failed to get records for listMissingBlobs", "error", err)
return helpers.ServerError(e, nil)
}
···
}
var count int64
+
if err := s.db.Raw(ctx, "SELECT COUNT(*) FROM blobs WHERE did = ? AND cid = ?", nil, urepo.Repo.Did, ref.cid.Bytes()).Scan(&count).Error; err != nil {
continue
}
+7 -4
server/handle_repo_list_records.go
···
}
func (s *Server) handleListRecords(e echo.Context) error {
var req ComAtprotoRepoListRecordsRequest
if err := e.Bind(&req); err != nil {
-
s.logger.Error("could not bind list records request", "error", err)
return helpers.ServerError(e, nil)
}
···
did := req.Repo
if _, err := syntax.ParseDID(did); err != nil {
-
actor, err := s.getActorByHandle(req.Repo)
if err != nil {
return helpers.InputError(e, to.StringPtr("RepoNotFound"))
}
···
params = append(params, limit)
var records []models.Record
-
if err := s.db.Raw("SELECT * FROM records WHERE did = ? AND nsid = ? "+cursorquery+" ORDER BY created_at "+sort+" limit ?", nil, params...).Scan(&records).Error; err != nil {
-
s.logger.Error("error getting records", "error", err)
return helpers.ServerError(e, nil)
}
···
}
func (s *Server) handleListRecords(e echo.Context) error {
+
ctx := e.Request().Context()
+
logger := s.logger.With("name", "handleListRecords")
+
var req ComAtprotoRepoListRecordsRequest
if err := e.Bind(&req); err != nil {
+
logger.Error("could not bind list records request", "error", err)
return helpers.ServerError(e, nil)
}
···
did := req.Repo
if _, err := syntax.ParseDID(did); err != nil {
+
actor, err := s.getActorByHandle(ctx, req.Repo)
if err != nil {
return helpers.InputError(e, to.StringPtr("RepoNotFound"))
}
···
params = append(params, limit)
var records []models.Record
+
if err := s.db.Raw(ctx, "SELECT * FROM records WHERE did = ? AND nsid = ? "+cursorquery+" ORDER BY created_at "+sort+" limit ?", nil, params...).Scan(&records).Error; err != nil {
+
logger.Error("error getting records", "error", err)
return helpers.ServerError(e, nil)
}
+3 -1
server/handle_repo_list_repos.go
···
// TODO: paginate this bitch
func (s *Server) handleListRepos(e echo.Context) error {
var repos []models.Repo
-
if err := s.db.Raw("SELECT * FROM repos ORDER BY created_at DESC LIMIT 500", nil).Scan(&repos).Error; err != nil {
return err
}
···
// TODO: paginate this bitch
func (s *Server) handleListRepos(e echo.Context) error {
+
ctx := e.Request().Context()
+
var repos []models.Repo
+
if err := s.db.Raw(ctx, "SELECT * FROM repos ORDER BY created_at DESC LIMIT 500", nil).Scan(&repos).Error; err != nil {
return err
}
+10 -7
server/handle_repo_put_record.go
···
"github.com/labstack/echo/v4"
)
-
type ComAtprotoRepoPutRecordRequest struct {
Repo string `json:"repo" validate:"required,atproto-did"`
Collection string `json:"collection" validate:"required,atproto-nsid"`
Rkey string `json:"rkey" validate:"required,atproto-rkey"`
···
}
func (s *Server) handlePutRecord(e echo.Context) error {
repo := e.Get("repo").(*models.RepoActor)
-
var req ComAtprotoRepoPutRecordRequest
if err := e.Bind(&req); err != nil {
-
s.logger.Error("error binding", "error", err)
return helpers.ServerError(e, nil)
}
if err := e.Validate(req); err != nil {
-
s.logger.Error("error validating", "error", err)
return helpers.InputError(e, nil)
}
if repo.Repo.Did != req.Repo {
-
s.logger.Warn("mismatched repo/auth")
return helpers.InputError(e, nil)
}
···
optype = OpTypeUpdate
}
-
results, err := s.repoman.applyWrites(repo.Repo, []Op{
{
Type: optype,
Collection: req.Collection,
···
},
}, req.SwapCommit)
if err != nil {
-
s.logger.Error("error applying writes", "error", err)
return helpers.ServerError(e, nil)
}
···
"github.com/labstack/echo/v4"
)
+
type ComAtprotoRepoPutRecordInput struct {
Repo string `json:"repo" validate:"required,atproto-did"`
Collection string `json:"collection" validate:"required,atproto-nsid"`
Rkey string `json:"rkey" validate:"required,atproto-rkey"`
···
}
func (s *Server) handlePutRecord(e echo.Context) error {
+
ctx := e.Request().Context()
+
logger := s.logger.With("name", "handlePutRecord")
+
repo := e.Get("repo").(*models.RepoActor)
+
var req ComAtprotoRepoPutRecordInput
if err := e.Bind(&req); err != nil {
+
logger.Error("error binding", "error", err)
return helpers.ServerError(e, nil)
}
if err := e.Validate(req); err != nil {
+
logger.Error("error validating", "error", err)
return helpers.InputError(e, nil)
}
if repo.Repo.Did != req.Repo {
+
logger.Warn("mismatched repo/auth")
return helpers.InputError(e, nil)
}
···
optype = OpTypeUpdate
}
+
results, err := s.repoman.applyWrites(ctx, repo.Repo, []Op{
{
Type: optype,
Collection: req.Collection,
···
},
}, req.SwapCommit)
if err != nil {
+
logger.Error("error applying writes", "error", err)
return helpers.ServerError(e, nil)
}
+13 -10
server/handle_repo_upload_blob.go
···
}
func (s *Server) handleRepoUploadBlob(e echo.Context) error {
urepo := e.Get("repo").(*models.RepoActor)
mime := e.Request().Header.Get("content-type")
···
Storage: storage,
}
-
if err := s.db.Create(&blob, nil).Error; err != nil {
-
s.logger.Error("error creating new blob in db", "error", err)
return helpers.ServerError(e, nil)
}
···
break
}
} else if err != nil && err != io.ErrUnexpectedEOF {
-
s.logger.Error("error reading blob", "error", err)
return helpers.ServerError(e, nil)
}
···
Data: data,
}
-
if err := s.db.Create(&blobPart, nil).Error; err != nil {
-
s.logger.Error("error adding blob part to db", "error", err)
return helpers.ServerError(e, nil)
}
}
···
c, err := cid.NewPrefixV1(cid.Raw, multihash.SHA2_256).Sum(fulldata.Bytes())
if err != nil {
-
s.logger.Error("error creating cid prefix", "error", err)
return helpers.ServerError(e, nil)
}
···
sess, err := session.NewSession(config)
if err != nil {
-
s.logger.Error("error creating aws session", "error", err)
return helpers.ServerError(e, nil)
}
···
Key: aws.String(fmt.Sprintf("blobs/%s/%s", urepo.Repo.Did, c.String())),
Body: bytes.NewReader(fulldata.Bytes()),
}); err != nil {
-
s.logger.Error("error uploading blob to s3", "error", err)
return helpers.ServerError(e, nil)
}
}
-
if err := s.db.Exec("UPDATE blobs SET cid = ? WHERE id = ?", nil, c.Bytes(), blob.ID).Error; err != nil {
// there should probably be somme handling here if this fails...
-
s.logger.Error("error updating blob", "error", err)
return helpers.ServerError(e, nil)
}
···
}
func (s *Server) handleRepoUploadBlob(e echo.Context) error {
+
ctx := e.Request().Context()
+
logger := s.logger.With("name", "handleRepoUploadBlob")
+
urepo := e.Get("repo").(*models.RepoActor)
mime := e.Request().Header.Get("content-type")
···
Storage: storage,
}
+
if err := s.db.Create(ctx, &blob, nil).Error; err != nil {
+
logger.Error("error creating new blob in db", "error", err)
return helpers.ServerError(e, nil)
}
···
break
}
} else if err != nil && err != io.ErrUnexpectedEOF {
+
logger.Error("error reading blob", "error", err)
return helpers.ServerError(e, nil)
}
···
Data: data,
}
+
if err := s.db.Create(ctx, &blobPart, nil).Error; err != nil {
+
logger.Error("error adding blob part to db", "error", err)
return helpers.ServerError(e, nil)
}
}
···
c, err := cid.NewPrefixV1(cid.Raw, multihash.SHA2_256).Sum(fulldata.Bytes())
if err != nil {
+
logger.Error("error creating cid prefix", "error", err)
return helpers.ServerError(e, nil)
}
···
sess, err := session.NewSession(config)
if err != nil {
+
logger.Error("error creating aws session", "error", err)
return helpers.ServerError(e, nil)
}
···
Key: aws.String(fmt.Sprintf("blobs/%s/%s", urepo.Repo.Did, c.String())),
Body: bytes.NewReader(fulldata.Bytes()),
}); err != nil {
+
logger.Error("error uploading blob to s3", "error", err)
return helpers.ServerError(e, nil)
}
}
+
if err := s.db.Exec(ctx, "UPDATE blobs SET cid = ? WHERE id = ?", nil, c.Bytes(), blob.ID).Error; err != nil {
// there should probably be somme handling here if this fails...
+
logger.Error("error updating blob", "error", err)
return helpers.ServerError(e, nil)
}
+6 -3
server/handle_server_activate_account.go
···
}
func (s *Server) handleServerActivateAccount(e echo.Context) error {
var req ComAtprotoServerDeactivateAccountRequest
if err := e.Bind(&req); err != nil {
-
s.logger.Error("error binding", "error", err)
return helpers.ServerError(e, nil)
}
urepo := e.Get("repo").(*models.RepoActor)
-
if err := s.db.Exec("UPDATE repos SET deactivated = ? WHERE did = ?", nil, false, urepo.Repo.Did).Error; err != nil {
-
s.logger.Error("error updating account status to deactivated", "error", err)
return helpers.ServerError(e, nil)
}
···
}
func (s *Server) handleServerActivateAccount(e echo.Context) error {
+
ctx := e.Request().Context()
+
logger := s.logger.With("name", "handleServerActivateAccount")
+
var req ComAtprotoServerDeactivateAccountRequest
if err := e.Bind(&req); err != nil {
+
logger.Error("error binding", "error", err)
return helpers.ServerError(e, nil)
}
urepo := e.Get("repo").(*models.RepoActor)
+
if err := s.db.Exec(ctx, "UPDATE repos SET deactivated = ? WHERE did = ?", nil, false, urepo.Repo.Did).Error; err != nil {
+
logger.Error("error updating account status to deactivated", "error", err)
return helpers.ServerError(e, nil)
}
+10 -7
server/handle_server_check_account_status.go
···
}
func (s *Server) handleServerCheckAccountStatus(e echo.Context) error {
urepo := e.Get("repo").(*models.RepoActor)
resp := ComAtprotoServerCheckAccountStatusResponse{
···
rootcid, err := cid.Cast(urepo.Root)
if err != nil {
-
s.logger.Error("error casting cid", "error", err)
return helpers.ServerError(e, nil)
}
resp.RepoCommit = rootcid.String()
···
}
var blockCtResp CountResp
-
if err := s.db.Raw("SELECT COUNT(*) AS ct FROM blocks WHERE did = ?", nil, urepo.Repo.Did).Scan(&blockCtResp).Error; err != nil {
-
s.logger.Error("error getting block count", "error", err)
return helpers.ServerError(e, nil)
}
resp.RepoBlocks = blockCtResp.Ct
var recCtResp CountResp
-
if err := s.db.Raw("SELECT COUNT(*) AS ct FROM records WHERE did = ?", nil, urepo.Repo.Did).Scan(&recCtResp).Error; err != nil {
-
s.logger.Error("error getting record count", "error", err)
return helpers.ServerError(e, nil)
}
resp.IndexedRecords = recCtResp.Ct
var blobCtResp CountResp
-
if err := s.db.Raw("SELECT COUNT(*) AS ct FROM blobs WHERE did = ?", nil, urepo.Repo.Did).Scan(&blobCtResp).Error; err != nil {
-
s.logger.Error("error getting record count", "error", err)
return helpers.ServerError(e, nil)
}
resp.ExpectedBlobs = blobCtResp.Ct
···
}
func (s *Server) handleServerCheckAccountStatus(e echo.Context) error {
+
ctx := e.Request().Context()
+
logger := s.logger.With("name", "handleServerCheckAccountStatus")
+
urepo := e.Get("repo").(*models.RepoActor)
resp := ComAtprotoServerCheckAccountStatusResponse{
···
rootcid, err := cid.Cast(urepo.Root)
if err != nil {
+
logger.Error("error casting cid", "error", err)
return helpers.ServerError(e, nil)
}
resp.RepoCommit = rootcid.String()
···
}
var blockCtResp CountResp
+
if err := s.db.Raw(ctx, "SELECT COUNT(*) AS ct FROM blocks WHERE did = ?", nil, urepo.Repo.Did).Scan(&blockCtResp).Error; err != nil {
+
logger.Error("error getting block count", "error", err)
return helpers.ServerError(e, nil)
}
resp.RepoBlocks = blockCtResp.Ct
var recCtResp CountResp
+
if err := s.db.Raw(ctx, "SELECT COUNT(*) AS ct FROM records WHERE did = ?", nil, urepo.Repo.Did).Scan(&recCtResp).Error; err != nil {
+
logger.Error("error getting record count", "error", err)
return helpers.ServerError(e, nil)
}
resp.IndexedRecords = recCtResp.Ct
var blobCtResp CountResp
+
if err := s.db.Raw(ctx, "SELECT COUNT(*) AS ct FROM blobs WHERE did = ?", nil, urepo.Repo.Did).Scan(&blobCtResp).Error; err != nil {
+
logger.Error("error getting record count", "error", err)
return helpers.ServerError(e, nil)
}
resp.ExpectedBlobs = blobCtResp.Ct
+6 -3
server/handle_server_confirm_email.go
···
}
func (s *Server) handleServerConfirmEmail(e echo.Context) error {
urepo := e.Get("repo").(*models.RepoActor)
var req ComAtprotoServerConfirmEmailRequest
if err := e.Bind(&req); err != nil {
-
s.logger.Error("error binding", "error", err)
return helpers.ServerError(e, nil)
}
···
now := time.Now().UTC()
-
if err := s.db.Exec("UPDATE repos SET email_verification_code = NULL, email_verification_code_expires_at = NULL, email_confirmed_at = ? WHERE did = ?", nil, now, urepo.Repo.Did).Error; err != nil {
-
s.logger.Error("error updating user", "error", err)
return helpers.ServerError(e, nil)
}
···
}
func (s *Server) handleServerConfirmEmail(e echo.Context) error {
+
ctx := e.Request().Context()
+
logger := s.logger.With("name", "handleServerConfirmEmail")
+
urepo := e.Get("repo").(*models.RepoActor)
var req ComAtprotoServerConfirmEmailRequest
if err := e.Bind(&req); err != nil {
+
logger.Error("error binding", "error", err)
return helpers.ServerError(e, nil)
}
···
now := time.Now().UTC()
+
if err := s.db.Exec(ctx, "UPDATE repos SET email_verification_code = NULL, email_verification_code_expires_at = NULL, email_confirmed_at = ? WHERE did = ?", nil, now, urepo.Repo.Did).Error; err != nil {
+
logger.Error("error updating user", "error", err)
return helpers.ServerError(e, nil)
}
+39 -36
server/handle_server_create_account.go
···
}
func (s *Server) handleCreateAccount(e echo.Context) error {
var request ComAtprotoServerCreateAccountRequest
if err := e.Bind(&request); err != nil {
-
s.logger.Error("error receiving request", "endpoint", "com.atproto.server.createAccount", "error", err)
return helpers.ServerError(e, nil)
}
request.Handle = strings.ToLower(request.Handle)
if err := e.Validate(request); err != nil {
-
s.logger.Error("error validating request", "endpoint", "com.atproto.server.createAccount", "error", err)
var verr ValidationError
if errors.As(err, &verr) {
···
}
}
}
-
var signupDid string
if request.Did != nil {
-
signupDid = *request.Did;
-
token := strings.TrimSpace(strings.Replace(e.Request().Header.Get("authorization"), "Bearer ", "", 1))
if token == "" {
return helpers.UnauthorizedError(e, to.StringPtr("must authenticate to use an existing did"))
···
authDid, err := s.validateServiceAuth(e.Request().Context(), token, "com.atproto.server.createAccount")
if err != nil {
-
s.logger.Warn("error validating authorization token", "endpoint", "com.atproto.server.createAccount", "error", err)
return helpers.UnauthorizedError(e, to.StringPtr("invalid authorization token"))
}
···
}
// see if the handle is already taken
-
actor, err := s.getActorByHandle(request.Handle)
if err != nil && err != gorm.ErrRecordNotFound {
-
s.logger.Error("error looking up handle in db", "endpoint", "com.atproto.server.createAccount", "error", err)
return helpers.ServerError(e, nil)
}
if err == nil && actor.Did != signupDid {
···
return helpers.InputError(e, to.StringPtr("InvalidInviteCode"))
}
-
if err := s.db.Raw("SELECT * FROM invite_codes WHERE code = ?", nil, request.InviteCode).Scan(&ic).Error; err != nil {
if err == gorm.ErrRecordNotFound {
return helpers.InputError(e, to.StringPtr("InvalidInviteCode"))
}
-
s.logger.Error("error getting invite code from db", "error", err)
return helpers.ServerError(e, nil)
}
···
}
// see if the email is already taken
-
existingRepo, err := s.getRepoByEmail(request.Email)
if err != nil && err != gorm.ErrRecordNotFound {
-
s.logger.Error("error looking up email in db", "endpoint", "com.atproto.server.createAccount", "error", err)
return helpers.ServerError(e, nil)
}
if err == nil && existingRepo.Did != signupDid {
···
var k *atcrypto.PrivateKeyK256
if signupDid != "" {
-
reservedKey, err := s.getReservedKey(signupDid)
if err != nil {
-
s.logger.Error("error looking up reserved key", "error", err)
}
if reservedKey != nil {
k, err = atcrypto.ParsePrivateBytesK256(reservedKey.PrivateKey)
if err != nil {
-
s.logger.Error("error parsing reserved key", "error", err)
k = nil
} else {
defer func() {
-
if delErr := s.deleteReservedKey(reservedKey.KeyDid, reservedKey.Did); delErr != nil {
-
s.logger.Error("error deleting reserved key", "error", delErr)
}
}()
}
···
if k == nil {
k, err = atcrypto.GeneratePrivateKeyK256()
if err != nil {
-
s.logger.Error("error creating signing key", "endpoint", "com.atproto.server.createAccount", "error", err)
return helpers.ServerError(e, nil)
}
}
···
if signupDid == "" {
did, op, err := s.plcClient.CreateDID(k, "", request.Handle)
if err != nil {
-
s.logger.Error("error creating operation", "endpoint", "com.atproto.server.createAccount", "error", err)
return helpers.ServerError(e, nil)
}
if err := s.plcClient.SendOperation(e.Request().Context(), did, op); err != nil {
-
s.logger.Error("error sending plc op", "endpoint", "com.atproto.server.createAccount", "error", err)
return helpers.ServerError(e, nil)
}
signupDid = did
···
hashed, err := bcrypt.GenerateFromPassword([]byte(request.Password), 10)
if err != nil {
-
s.logger.Error("error hashing password", "error", err)
return helpers.ServerError(e, nil)
}
···
Handle: request.Handle,
}
-
if err := s.db.Create(&urepo, nil).Error; err != nil {
-
s.logger.Error("error inserting new repo", "error", err)
return helpers.ServerError(e, nil)
}
-
-
if err := s.db.Create(&actor, nil).Error; err != nil {
-
s.logger.Error("error inserting new actor", "error", err)
return helpers.ServerError(e, nil)
}
} else {
-
if err := s.db.Save(&actor, nil).Error; err != nil {
-
s.logger.Error("error inserting new actor", "error", err)
return helpers.ServerError(e, nil)
}
}
···
root, rev, err := r.Commit(context.TODO(), urepo.SignFor)
if err != nil {
-
s.logger.Error("error committing", "error", err)
return helpers.ServerError(e, nil)
}
if err := s.UpdateRepo(context.TODO(), urepo.Did, root, rev); err != nil {
-
s.logger.Error("error updating repo after commit", "error", err)
return helpers.ServerError(e, nil)
}
···
}
if s.config.RequireInvite {
-
if err := s.db.Raw("UPDATE invite_codes SET remaining_use_count = remaining_use_count - 1 WHERE code = ?", nil, request.InviteCode).Scan(&ic).Error; err != nil {
-
s.logger.Error("error decrementing use count", "error", err)
return helpers.ServerError(e, nil)
}
}
-
sess, err := s.createSession(&urepo)
if err != nil {
-
s.logger.Error("error creating new session", "error", err)
return helpers.ServerError(e, nil)
}
go func() {
if err := s.sendEmailVerification(urepo.Email, actor.Handle, *urepo.EmailVerificationCode); err != nil {
-
s.logger.Error("error sending email verification email", "error", err)
}
if err := s.sendWelcomeMail(urepo.Email, actor.Handle); err != nil {
-
s.logger.Error("error sending welcome email", "error", err)
}
}()
···
}
func (s *Server) handleCreateAccount(e echo.Context) error {
+
ctx := e.Request().Context()
+
logger := s.logger.With("name", "handleServerCreateAccount")
+
var request ComAtprotoServerCreateAccountRequest
if err := e.Bind(&request); err != nil {
+
logger.Error("error receiving request", "endpoint", "com.atproto.server.createAccount", "error", err)
return helpers.ServerError(e, nil)
}
request.Handle = strings.ToLower(request.Handle)
if err := e.Validate(request); err != nil {
+
logger.Error("error validating request", "endpoint", "com.atproto.server.createAccount", "error", err)
var verr ValidationError
if errors.As(err, &verr) {
···
}
}
}
+
var signupDid string
if request.Did != nil {
+
signupDid = *request.Did
+
token := strings.TrimSpace(strings.Replace(e.Request().Header.Get("authorization"), "Bearer ", "", 1))
if token == "" {
return helpers.UnauthorizedError(e, to.StringPtr("must authenticate to use an existing did"))
···
authDid, err := s.validateServiceAuth(e.Request().Context(), token, "com.atproto.server.createAccount")
if err != nil {
+
logger.Warn("error validating authorization token", "endpoint", "com.atproto.server.createAccount", "error", err)
return helpers.UnauthorizedError(e, to.StringPtr("invalid authorization token"))
}
···
}
// see if the handle is already taken
+
actor, err := s.getActorByHandle(ctx, request.Handle)
if err != nil && err != gorm.ErrRecordNotFound {
+
logger.Error("error looking up handle in db", "endpoint", "com.atproto.server.createAccount", "error", err)
return helpers.ServerError(e, nil)
}
if err == nil && actor.Did != signupDid {
···
return helpers.InputError(e, to.StringPtr("InvalidInviteCode"))
}
+
if err := s.db.Raw(ctx, "SELECT * FROM invite_codes WHERE code = ?", nil, request.InviteCode).Scan(&ic).Error; err != nil {
if err == gorm.ErrRecordNotFound {
return helpers.InputError(e, to.StringPtr("InvalidInviteCode"))
}
+
logger.Error("error getting invite code from db", "error", err)
return helpers.ServerError(e, nil)
}
···
}
// see if the email is already taken
+
existingRepo, err := s.getRepoByEmail(ctx, request.Email)
if err != nil && err != gorm.ErrRecordNotFound {
+
logger.Error("error looking up email in db", "endpoint", "com.atproto.server.createAccount", "error", err)
return helpers.ServerError(e, nil)
}
if err == nil && existingRepo.Did != signupDid {
···
var k *atcrypto.PrivateKeyK256
if signupDid != "" {
+
reservedKey, err := s.getReservedKey(ctx, signupDid)
if err != nil {
+
logger.Error("error looking up reserved key", "error", err)
}
if reservedKey != nil {
k, err = atcrypto.ParsePrivateBytesK256(reservedKey.PrivateKey)
if err != nil {
+
logger.Error("error parsing reserved key", "error", err)
k = nil
} else {
defer func() {
+
if delErr := s.deleteReservedKey(ctx, reservedKey.KeyDid, reservedKey.Did); delErr != nil {
+
logger.Error("error deleting reserved key", "error", delErr)
}
}()
}
···
if k == nil {
k, err = atcrypto.GeneratePrivateKeyK256()
if err != nil {
+
logger.Error("error creating signing key", "endpoint", "com.atproto.server.createAccount", "error", err)
return helpers.ServerError(e, nil)
}
}
···
if signupDid == "" {
did, op, err := s.plcClient.CreateDID(k, "", request.Handle)
if err != nil {
+
logger.Error("error creating operation", "endpoint", "com.atproto.server.createAccount", "error", err)
return helpers.ServerError(e, nil)
}
if err := s.plcClient.SendOperation(e.Request().Context(), did, op); err != nil {
+
logger.Error("error sending plc op", "endpoint", "com.atproto.server.createAccount", "error", err)
return helpers.ServerError(e, nil)
}
signupDid = did
···
hashed, err := bcrypt.GenerateFromPassword([]byte(request.Password), 10)
if err != nil {
+
logger.Error("error hashing password", "error", err)
return helpers.ServerError(e, nil)
}
···
Handle: request.Handle,
}
+
if err := s.db.Create(ctx, &urepo, nil).Error; err != nil {
+
logger.Error("error inserting new repo", "error", err)
return helpers.ServerError(e, nil)
}
+
+
if err := s.db.Create(ctx, &actor, nil).Error; err != nil {
+
logger.Error("error inserting new actor", "error", err)
return helpers.ServerError(e, nil)
}
} else {
+
if err := s.db.Save(ctx, &actor, nil).Error; err != nil {
+
logger.Error("error inserting new actor", "error", err)
return helpers.ServerError(e, nil)
}
}
···
root, rev, err := r.Commit(context.TODO(), urepo.SignFor)
if err != nil {
+
logger.Error("error committing", "error", err)
return helpers.ServerError(e, nil)
}
if err := s.UpdateRepo(context.TODO(), urepo.Did, root, rev); err != nil {
+
logger.Error("error updating repo after commit", "error", err)
return helpers.ServerError(e, nil)
}
···
}
if s.config.RequireInvite {
+
if err := s.db.Raw(ctx, "UPDATE invite_codes SET remaining_use_count = remaining_use_count - 1 WHERE code = ?", nil, request.InviteCode).Scan(&ic).Error; err != nil {
+
logger.Error("error decrementing use count", "error", err)
return helpers.ServerError(e, nil)
}
}
+
sess, err := s.createSession(ctx, &urepo)
if err != nil {
+
logger.Error("error creating new session", "error", err)
return helpers.ServerError(e, nil)
}
go func() {
if err := s.sendEmailVerification(urepo.Email, actor.Handle, *urepo.EmailVerificationCode); err != nil {
+
logger.Error("error sending email verification email", "error", err)
}
if err := s.sendWelcomeMail(urepo.Email, actor.Handle); err != nil {
+
logger.Error("error sending welcome email", "error", err)
}
}()
+7 -4
server/handle_server_create_invite_code.go
···
}
func (s *Server) handleCreateInviteCode(e echo.Context) error {
var req ComAtprotoServerCreateInviteCodeRequest
if err := e.Bind(&req); err != nil {
-
s.logger.Error("error binding", "error", err)
return helpers.ServerError(e, nil)
}
if err := e.Validate(req); err != nil {
-
s.logger.Error("error validating", "error", err)
return helpers.InputError(e, nil)
}
···
acc = *req.ForAccount
}
-
if err := s.db.Create(&models.InviteCode{
Code: ic,
Did: acc,
RemainingUseCount: req.UseCount,
}, nil).Error; err != nil {
-
s.logger.Error("error creating invite code", "error", err)
return helpers.ServerError(e, nil)
}
···
}
func (s *Server) handleCreateInviteCode(e echo.Context) error {
+
ctx := e.Request().Context()
+
logger := s.logger.With("name", "handleServerCreateInviteCode")
+
var req ComAtprotoServerCreateInviteCodeRequest
if err := e.Bind(&req); err != nil {
+
logger.Error("error binding", "error", err)
return helpers.ServerError(e, nil)
}
if err := e.Validate(req); err != nil {
+
logger.Error("error validating", "error", err)
return helpers.InputError(e, nil)
}
···
acc = *req.ForAccount
}
+
if err := s.db.Create(ctx, &models.InviteCode{
Code: ic,
Did: acc,
RemainingUseCount: req.UseCount,
}, nil).Error; err != nil {
+
logger.Error("error creating invite code", "error", err)
return helpers.ServerError(e, nil)
}
+7 -4
server/handle_server_create_invite_codes.go
···
}
func (s *Server) handleCreateInviteCodes(e echo.Context) error {
var req ComAtprotoServerCreateInviteCodesRequest
if err := e.Bind(&req); err != nil {
-
s.logger.Error("error binding", "error", err)
return helpers.ServerError(e, nil)
}
if err := e.Validate(req); err != nil {
-
s.logger.Error("error validating", "error", err)
return helpers.InputError(e, nil)
}
···
ic := uuid.NewString()
ics = append(ics, ic)
-
if err := s.db.Create(&models.InviteCode{
Code: ic,
Did: did,
RemainingUseCount: req.UseCount,
}, nil).Error; err != nil {
-
s.logger.Error("error creating invite code", "error", err)
return helpers.ServerError(e, nil)
}
}
···
}
func (s *Server) handleCreateInviteCodes(e echo.Context) error {
+
ctx := e.Request().Context()
+
logger := s.logger.With("name", "handleServerCreateInviteCodes")
+
var req ComAtprotoServerCreateInviteCodesRequest
if err := e.Bind(&req); err != nil {
+
logger.Error("error binding", "error", err)
return helpers.ServerError(e, nil)
}
if err := e.Validate(req); err != nil {
+
logger.Error("error validating", "error", err)
return helpers.InputError(e, nil)
}
···
ic := uuid.NewString()
ics = append(ics, ic)
+
if err := s.db.Create(ctx, &models.InviteCode{
Code: ic,
Did: did,
RemainingUseCount: req.UseCount,
}, nil).Error; err != nil {
+
logger.Error("error creating invite code", "error", err)
return helpers.ServerError(e, nil)
}
}
+65 -9
server/handle_server_create_session.go
···
package server
import (
"errors"
"strings"
"github.com/Azure/go-autorest/autorest/to"
"github.com/bluesky-social/indigo/atproto/syntax"
···
}
func (s *Server) handleCreateSession(e echo.Context) error {
var req ComAtprotoServerCreateSessionRequest
if err := e.Bind(&req); err != nil {
-
s.logger.Error("error binding request", "endpoint", "com.atproto.server.serverCreateSession", "error", err)
return helpers.ServerError(e, nil)
}
···
var err error
switch idtype {
case "did":
-
err = s.db.Raw("SELECT r.*, a.* FROM repos r LEFT JOIN actors a ON r.did = a.did WHERE r.did = ?", nil, req.Identifier).Scan(&repo).Error
case "handle":
-
err = s.db.Raw("SELECT r.*, a.* FROM actors a LEFT JOIN repos r ON a.did = r.did WHERE a.handle = ?", nil, req.Identifier).Scan(&repo).Error
case "email":
-
err = s.db.Raw("SELECT r.*, a.* FROM repos r LEFT JOIN actors a ON r.did = a.did WHERE r.email = ?", nil, req.Identifier).Scan(&repo).Error
}
if err != nil {
···
return helpers.InputError(e, to.StringPtr("InvalidRequest"))
}
-
s.logger.Error("erorr looking up repo", "endpoint", "com.atproto.server.createSession", "error", err)
return helpers.ServerError(e, nil)
}
if err := bcrypt.CompareHashAndPassword([]byte(repo.Password), []byte(req.Password)); err != nil {
if err != bcrypt.ErrMismatchedHashAndPassword {
-
s.logger.Error("erorr comparing hash and password", "error", err)
}
return helpers.InputError(e, to.StringPtr("InvalidRequest"))
}
-
sess, err := s.createSession(&repo.Repo)
if err != nil {
-
s.logger.Error("error creating session", "error", err)
return helpers.ServerError(e, nil)
}
···
Did: repo.Repo.Did,
Email: repo.Email,
EmailConfirmed: repo.EmailConfirmedAt != nil,
-
EmailAuthFactor: false,
Active: repo.Active(),
Status: repo.Status(),
})
}
···
package server
import (
+
"context"
"errors"
+
"fmt"
"strings"
+
"time"
"github.com/Azure/go-autorest/autorest/to"
"github.com/bluesky-social/indigo/atproto/syntax"
···
}
func (s *Server) handleCreateSession(e echo.Context) error {
+
ctx := e.Request().Context()
+
logger := s.logger.With("name", "handleServerCreateSession")
+
var req ComAtprotoServerCreateSessionRequest
if err := e.Bind(&req); err != nil {
+
logger.Error("error binding request", "endpoint", "com.atproto.server.serverCreateSession", "error", err)
return helpers.ServerError(e, nil)
}
···
var err error
switch idtype {
case "did":
+
err = s.db.Raw(ctx, "SELECT r.*, a.* FROM repos r LEFT JOIN actors a ON r.did = a.did WHERE r.did = ?", nil, req.Identifier).Scan(&repo).Error
case "handle":
+
err = s.db.Raw(ctx, "SELECT r.*, a.* FROM actors a LEFT JOIN repos r ON a.did = r.did WHERE a.handle = ?", nil, req.Identifier).Scan(&repo).Error
case "email":
+
err = s.db.Raw(ctx, "SELECT r.*, a.* FROM repos r LEFT JOIN actors a ON r.did = a.did WHERE r.email = ?", nil, req.Identifier).Scan(&repo).Error
}
if err != nil {
···
return helpers.InputError(e, to.StringPtr("InvalidRequest"))
}
+
logger.Error("erorr looking up repo", "endpoint", "com.atproto.server.createSession", "error", err)
return helpers.ServerError(e, nil)
}
if err := bcrypt.CompareHashAndPassword([]byte(repo.Password), []byte(req.Password)); err != nil {
if err != bcrypt.ErrMismatchedHashAndPassword {
+
logger.Error("erorr comparing hash and password", "error", err)
}
return helpers.InputError(e, to.StringPtr("InvalidRequest"))
}
+
// if repo requires 2FA token and one hasn't been provided, return error prompting for one
+
if repo.TwoFactorType != models.TwoFactorTypeNone && (req.AuthFactorToken == nil || *req.AuthFactorToken == "") {
+
err = s.createAndSendTwoFactorCode(ctx, repo)
+
if err != nil {
+
logger.Error("sending 2FA code", "error", err)
+
return helpers.ServerError(e, nil)
+
}
+
+
return helpers.InputError(e, to.StringPtr("AuthFactorTokenRequired"))
+
}
+
+
// if 2FA is required, now check that the one provided is valid
+
if repo.TwoFactorType != models.TwoFactorTypeNone {
+
if repo.TwoFactorCode == nil || repo.TwoFactorCodeExpiresAt == nil {
+
err = s.createAndSendTwoFactorCode(ctx, repo)
+
if err != nil {
+
logger.Error("sending 2FA code", "error", err)
+
return helpers.ServerError(e, nil)
+
}
+
+
return helpers.InputError(e, to.StringPtr("AuthFactorTokenRequired"))
+
}
+
+
if *repo.TwoFactorCode != *req.AuthFactorToken {
+
return helpers.InvalidTokenError(e)
+
}
+
+
if time.Now().UTC().After(*repo.TwoFactorCodeExpiresAt) {
+
return helpers.ExpiredTokenError(e)
+
}
+
}
+
+
sess, err := s.createSession(ctx, &repo.Repo)
if err != nil {
+
logger.Error("error creating session", "error", err)
return helpers.ServerError(e, nil)
}
···
Did: repo.Repo.Did,
Email: repo.Email,
EmailConfirmed: repo.EmailConfirmedAt != nil,
+
EmailAuthFactor: repo.TwoFactorType != models.TwoFactorTypeNone,
Active: repo.Active(),
Status: repo.Status(),
})
}
+
+
func (s *Server) createAndSendTwoFactorCode(ctx context.Context, repo models.RepoActor) error {
+
// TODO: when implementing a new type of 2FA there should be some logic in here to send the
+
// right type of code
+
+
code := fmt.Sprintf("%s-%s", helpers.RandomVarchar(5), helpers.RandomVarchar(5))
+
eat := time.Now().Add(10 * time.Minute).UTC()
+
+
if err := s.db.Exec(ctx, "UPDATE repos SET two_factor_code = ?, two_factor_code_expires_at = ? WHERE did = ?", nil, code, eat, repo.Repo.Did).Error; err != nil {
+
return fmt.Errorf("updating repo: %w", err)
+
}
+
+
if err := s.sendTwoFactorCode(repo.Email, repo.Handle, code); err != nil {
+
return fmt.Errorf("sending email: %w", err)
+
}
+
+
return nil
+
}
+6 -3
server/handle_server_deactivate_account.go
···
}
func (s *Server) handleServerDeactivateAccount(e echo.Context) error {
var req ComAtprotoServerDeactivateAccountRequest
if err := e.Bind(&req); err != nil {
-
s.logger.Error("error binding", "error", err)
return helpers.ServerError(e, nil)
}
urepo := e.Get("repo").(*models.RepoActor)
-
if err := s.db.Exec("UPDATE repos SET deactivated = ? WHERE did = ?", nil, true, urepo.Repo.Did).Error; err != nil {
-
s.logger.Error("error updating account status to deactivated", "error", err)
return helpers.ServerError(e, nil)
}
···
}
func (s *Server) handleServerDeactivateAccount(e echo.Context) error {
+
ctx := e.Request().Context()
+
logger := s.logger.With("name", "handleServerDeactivateAccount")
+
var req ComAtprotoServerDeactivateAccountRequest
if err := e.Bind(&req); err != nil {
+
logger.Error("error binding", "error", err)
return helpers.ServerError(e, nil)
}
urepo := e.Get("repo").(*models.RepoActor)
+
if err := s.db.Exec(ctx, "UPDATE repos SET deactivated = ? WHERE did = ?", nil, true, urepo.Repo.Did).Error; err != nil {
+
logger.Error("error updating account status to deactivated", "error", err)
return helpers.ServerError(e, nil)
}
+34 -29
server/handle_server_delete_account.go
···
}
func (s *Server) handleServerDeleteAccount(e echo.Context) error {
var req ComAtprotoServerDeleteAccountRequest
if err := e.Bind(&req); err != nil {
-
s.logger.Error("error binding", "error", err)
return helpers.ServerError(e, nil)
}
if err := e.Validate(&req); err != nil {
-
s.logger.Error("error validating", "error", err)
return helpers.ServerError(e, nil)
}
-
urepo, err := s.getRepoActorByDid(req.Did)
if err != nil {
-
s.logger.Error("error getting repo", "error", err)
return echo.NewHTTPError(400, "account not found")
}
if err := bcrypt.CompareHashAndPassword([]byte(urepo.Repo.Password), []byte(req.Password)); err != nil {
-
s.logger.Error("password mismatch", "error", err)
return echo.NewHTTPError(401, "Invalid did or password")
}
if urepo.Repo.AccountDeleteCode == nil || urepo.Repo.AccountDeleteCodeExpiresAt == nil {
-
s.logger.Error("no deletion token found for account")
return echo.NewHTTPError(400, map[string]interface{}{
"error": "InvalidToken",
"message": "Token is invalid",
···
}
if *urepo.Repo.AccountDeleteCode != req.Token {
-
s.logger.Error("deletion token mismatch")
return echo.NewHTTPError(400, map[string]interface{}{
"error": "InvalidToken",
"message": "Token is invalid",
···
}
if time.Now().UTC().After(*urepo.Repo.AccountDeleteCodeExpiresAt) {
-
s.logger.Error("deletion token expired")
return echo.NewHTTPError(400, map[string]interface{}{
"error": "ExpiredToken",
"message": "Token is expired",
})
}
-
tx := s.db.BeginDangerously()
if tx.Error != nil {
-
s.logger.Error("error starting transaction", "error", tx.Error)
return helpers.ServerError(e, nil)
}
if err := tx.Exec("DELETE FROM blocks WHERE did = ?", nil, req.Did).Error; err != nil {
-
tx.Rollback()
-
s.logger.Error("error deleting blocks", "error", err)
return helpers.ServerError(e, nil)
}
if err := tx.Exec("DELETE FROM records WHERE did = ?", nil, req.Did).Error; err != nil {
-
tx.Rollback()
-
s.logger.Error("error deleting records", "error", err)
return helpers.ServerError(e, nil)
}
if err := tx.Exec("DELETE FROM blobs WHERE did = ?", nil, req.Did).Error; err != nil {
-
tx.Rollback()
-
s.logger.Error("error deleting blobs", "error", err)
return helpers.ServerError(e, nil)
}
if err := tx.Exec("DELETE FROM tokens WHERE did = ?", nil, req.Did).Error; err != nil {
-
tx.Rollback()
-
s.logger.Error("error deleting tokens", "error", err)
return helpers.ServerError(e, nil)
}
if err := tx.Exec("DELETE FROM refresh_tokens WHERE did = ?", nil, req.Did).Error; err != nil {
-
tx.Rollback()
-
s.logger.Error("error deleting refresh tokens", "error", err)
return helpers.ServerError(e, nil)
}
if err := tx.Exec("DELETE FROM reserved_keys WHERE did = ?", nil, req.Did).Error; err != nil {
-
tx.Rollback()
-
s.logger.Error("error deleting reserved keys", "error", err)
return helpers.ServerError(e, nil)
}
if err := tx.Exec("DELETE FROM invite_codes WHERE did = ?", nil, req.Did).Error; err != nil {
-
tx.Rollback()
-
s.logger.Error("error deleting invite codes", "error", err)
return helpers.ServerError(e, nil)
}
if err := tx.Exec("DELETE FROM actors WHERE did = ?", nil, req.Did).Error; err != nil {
-
tx.Rollback()
-
s.logger.Error("error deleting actor", "error", err)
return helpers.ServerError(e, nil)
}
if err := tx.Exec("DELETE FROM repos WHERE did = ?", nil, req.Did).Error; err != nil {
-
tx.Rollback()
-
s.logger.Error("error deleting repo", "error", err)
return helpers.ServerError(e, nil)
}
if err := tx.Commit().Error; err != nil {
-
s.logger.Error("error committing transaction", "error", err)
return helpers.ServerError(e, nil)
}
···
}
func (s *Server) handleServerDeleteAccount(e echo.Context) error {
+
ctx := e.Request().Context()
+
logger := s.logger.With("name", "handleServerDeleteAccount")
+
var req ComAtprotoServerDeleteAccountRequest
if err := e.Bind(&req); err != nil {
+
logger.Error("error binding", "error", err)
return helpers.ServerError(e, nil)
}
if err := e.Validate(&req); err != nil {
+
logger.Error("error validating", "error", err)
return helpers.ServerError(e, nil)
}
+
urepo, err := s.getRepoActorByDid(ctx, req.Did)
if err != nil {
+
logger.Error("error getting repo", "error", err)
return echo.NewHTTPError(400, "account not found")
}
if err := bcrypt.CompareHashAndPassword([]byte(urepo.Repo.Password), []byte(req.Password)); err != nil {
+
logger.Error("password mismatch", "error", err)
return echo.NewHTTPError(401, "Invalid did or password")
}
if urepo.Repo.AccountDeleteCode == nil || urepo.Repo.AccountDeleteCodeExpiresAt == nil {
+
logger.Error("no deletion token found for account")
return echo.NewHTTPError(400, map[string]interface{}{
"error": "InvalidToken",
"message": "Token is invalid",
···
}
if *urepo.Repo.AccountDeleteCode != req.Token {
+
logger.Error("deletion token mismatch")
return echo.NewHTTPError(400, map[string]interface{}{
"error": "InvalidToken",
"message": "Token is invalid",
···
}
if time.Now().UTC().After(*urepo.Repo.AccountDeleteCodeExpiresAt) {
+
logger.Error("deletion token expired")
return echo.NewHTTPError(400, map[string]interface{}{
"error": "ExpiredToken",
"message": "Token is expired",
})
}
+
tx := s.db.BeginDangerously(ctx)
if tx.Error != nil {
+
logger.Error("error starting transaction", "error", tx.Error)
return helpers.ServerError(e, nil)
}
+
+
status := "error"
+
func() {
+
if status == "error" {
+
if err := tx.Rollback().Error; err != nil {
+
logger.Error("error rolling back after delete failure", "err", err)
+
}
+
}
+
}()
if err := tx.Exec("DELETE FROM blocks WHERE did = ?", nil, req.Did).Error; err != nil {
+
logger.Error("error deleting blocks", "error", err)
return helpers.ServerError(e, nil)
}
if err := tx.Exec("DELETE FROM records WHERE did = ?", nil, req.Did).Error; err != nil {
+
logger.Error("error deleting records", "error", err)
return helpers.ServerError(e, nil)
}
if err := tx.Exec("DELETE FROM blobs WHERE did = ?", nil, req.Did).Error; err != nil {
+
logger.Error("error deleting blobs", "error", err)
return helpers.ServerError(e, nil)
}
if err := tx.Exec("DELETE FROM tokens WHERE did = ?", nil, req.Did).Error; err != nil {
+
logger.Error("error deleting tokens", "error", err)
return helpers.ServerError(e, nil)
}
if err := tx.Exec("DELETE FROM refresh_tokens WHERE did = ?", nil, req.Did).Error; err != nil {
+
logger.Error("error deleting refresh tokens", "error", err)
return helpers.ServerError(e, nil)
}
if err := tx.Exec("DELETE FROM reserved_keys WHERE did = ?", nil, req.Did).Error; err != nil {
+
logger.Error("error deleting reserved keys", "error", err)
return helpers.ServerError(e, nil)
}
if err := tx.Exec("DELETE FROM invite_codes WHERE did = ?", nil, req.Did).Error; err != nil {
+
logger.Error("error deleting invite codes", "error", err)
return helpers.ServerError(e, nil)
}
if err := tx.Exec("DELETE FROM actors WHERE did = ?", nil, req.Did).Error; err != nil {
+
logger.Error("error deleting actor", "error", err)
return helpers.ServerError(e, nil)
}
if err := tx.Exec("DELETE FROM repos WHERE did = ?", nil, req.Did).Error; err != nil {
+
logger.Error("error deleting repo", "error", err)
return helpers.ServerError(e, nil)
}
+
status = "ok"
+
if err := tx.Commit().Error; err != nil {
+
logger.Error("error committing transaction", "error", err)
return helpers.ServerError(e, nil)
}
+4 -2
server/handle_server_delete_session.go
···
)
func (s *Server) handleDeleteSession(e echo.Context) error {
token := e.Get("token").(string)
var acctok models.Token
-
if err := s.db.Raw("DELETE FROM tokens WHERE token = ? RETURNING *", nil, token).Scan(&acctok).Error; err != nil {
s.logger.Error("error deleting access token from db", "error", err)
return helpers.ServerError(e, nil)
}
-
if err := s.db.Exec("DELETE FROM refresh_tokens WHERE token = ?", nil, acctok.RefreshToken).Error; err != nil {
s.logger.Error("error deleting refresh token from db", "error", err)
return helpers.ServerError(e, nil)
}
···
)
func (s *Server) handleDeleteSession(e echo.Context) error {
+
ctx := e.Request().Context()
+
token := e.Get("token").(string)
var acctok models.Token
+
if err := s.db.Raw(ctx, "DELETE FROM tokens WHERE token = ? RETURNING *", nil, token).Scan(&acctok).Error; err != nil {
s.logger.Error("error deleting access token from db", "error", err)
return helpers.ServerError(e, nil)
}
+
if err := s.db.Exec(ctx, "DELETE FROM refresh_tokens WHERE token = ?", nil, acctok.RefreshToken).Error; err != nil {
s.logger.Error("error deleting refresh token from db", "error", err)
return helpers.ServerError(e, nil)
}
+7 -5
server/handle_server_get_service_auth.go
···
}
func (s *Server) handleServerGetServiceAuth(e echo.Context) error {
var req ServerGetServiceAuthRequest
if err := e.Bind(&req); err != nil {
-
s.logger.Error("could not bind service auth request", "error", err)
return helpers.ServerError(e, nil)
}
···
}
hj, err := json.Marshal(header)
if err != nil {
-
s.logger.Error("error marshaling header", "error", err)
return helpers.ServerError(e, nil)
}
···
}
pj, err := json.Marshal(payload)
if err != nil {
-
s.logger.Error("error marashaling payload", "error", err)
return helpers.ServerError(e, nil)
}
···
sk, err := secp256k1secec.NewPrivateKey(repo.SigningKey)
if err != nil {
-
s.logger.Error("can't load private key", "error", err)
return err
}
R, S, _, err := sk.SignRaw(rand.Reader, hash[:])
if err != nil {
-
s.logger.Error("error signing", "error", err)
return helpers.ServerError(e, nil)
}
···
}
func (s *Server) handleServerGetServiceAuth(e echo.Context) error {
+
logger := s.logger.With("name", "handleServerGetServiceAuth")
+
var req ServerGetServiceAuthRequest
if err := e.Bind(&req); err != nil {
+
logger.Error("could not bind service auth request", "error", err)
return helpers.ServerError(e, nil)
}
···
}
hj, err := json.Marshal(header)
if err != nil {
+
logger.Error("error marshaling header", "error", err)
return helpers.ServerError(e, nil)
}
···
}
pj, err := json.Marshal(payload)
if err != nil {
+
logger.Error("error marashaling payload", "error", err)
return helpers.ServerError(e, nil)
}
···
sk, err := secp256k1secec.NewPrivateKey(repo.SigningKey)
if err != nil {
+
logger.Error("can't load private key", "error", err)
return err
}
R, S, _, err := sk.SignRaw(rand.Reader, hash[:])
if err != nil {
+
logger.Error("error signing", "error", err)
return helpers.ServerError(e, nil)
}
+1 -1
server/handle_server_get_session.go
···
Did: repo.Repo.Did,
Email: repo.Email,
EmailConfirmed: repo.EmailConfirmedAt != nil,
-
EmailAuthFactor: false, // TODO: todo todo
Active: repo.Active(),
Status: repo.Status(),
})
···
Did: repo.Repo.Did,
Email: repo.Email,
EmailConfirmed: repo.EmailConfirmedAt != nil,
+
EmailAuthFactor: repo.TwoFactorType != models.TwoFactorTypeNone,
Active: repo.Active(),
Status: repo.Status(),
})
+9 -6
server/handle_server_refresh_session.go
···
}
func (s *Server) handleRefreshSession(e echo.Context) error {
token := e.Get("token").(string)
repo := e.Get("repo").(*models.RepoActor)
-
if err := s.db.Exec("DELETE FROM refresh_tokens WHERE token = ?", nil, token).Error; err != nil {
-
s.logger.Error("error getting refresh token from db", "error", err)
return helpers.ServerError(e, nil)
}
-
if err := s.db.Exec("DELETE FROM tokens WHERE refresh_token = ?", nil, token).Error; err != nil {
-
s.logger.Error("error deleting access token from db", "error", err)
return helpers.ServerError(e, nil)
}
-
sess, err := s.createSession(&repo.Repo)
if err != nil {
-
s.logger.Error("error creating new session for refresh", "error", err)
return helpers.ServerError(e, nil)
}
···
}
func (s *Server) handleRefreshSession(e echo.Context) error {
+
ctx := e.Request().Context()
+
logger := s.logger.With("name", "handleServerRefreshSession")
+
token := e.Get("token").(string)
repo := e.Get("repo").(*models.RepoActor)
+
if err := s.db.Exec(ctx, "DELETE FROM refresh_tokens WHERE token = ?", nil, token).Error; err != nil {
+
logger.Error("error getting refresh token from db", "error", err)
return helpers.ServerError(e, nil)
}
+
if err := s.db.Exec(ctx, "DELETE FROM tokens WHERE refresh_token = ?", nil, token).Error; err != nil {
+
logger.Error("error deleting access token from db", "error", err)
return helpers.ServerError(e, nil)
}
+
sess, err := s.createSession(ctx, &repo.Repo)
if err != nil {
+
logger.Error("error creating new session for refresh", "error", err)
return helpers.ServerError(e, nil)
}
+6 -3
server/handle_server_request_account_delete.go
···
)
func (s *Server) handleServerRequestAccountDelete(e echo.Context) error {
urepo := e.Get("repo").(*models.RepoActor)
token := fmt.Sprintf("%s-%s", helpers.RandomVarchar(5), helpers.RandomVarchar(5))
expiresAt := time.Now().UTC().Add(15 * time.Minute)
-
if err := s.db.Exec("UPDATE repos SET account_delete_code = ?, account_delete_code_expires_at = ? WHERE did = ?", nil, token, expiresAt, urepo.Repo.Did).Error; err != nil {
-
s.logger.Error("error setting deletion token", "error", err)
return helpers.ServerError(e, nil)
}
if urepo.Email != "" {
if err := s.sendAccountDeleteEmail(urepo.Email, urepo.Actor.Handle, token); err != nil {
-
s.logger.Error("error sending account deletion email", "error", err)
}
}
···
)
func (s *Server) handleServerRequestAccountDelete(e echo.Context) error {
+
ctx := e.Request().Context()
+
logger := s.logger.With("name", "handleServerRequestAccountDelete")
+
urepo := e.Get("repo").(*models.RepoActor)
token := fmt.Sprintf("%s-%s", helpers.RandomVarchar(5), helpers.RandomVarchar(5))
expiresAt := time.Now().UTC().Add(15 * time.Minute)
+
if err := s.db.Exec(ctx, "UPDATE repos SET account_delete_code = ?, account_delete_code_expires_at = ? WHERE did = ?", nil, token, expiresAt, urepo.Repo.Did).Error; err != nil {
+
logger.Error("error setting deletion token", "error", err)
return helpers.ServerError(e, nil)
}
if urepo.Email != "" {
if err := s.sendAccountDeleteEmail(urepo.Email, urepo.Actor.Handle, token); err != nil {
+
logger.Error("error sending account deletion email", "error", err)
}
}
+6 -3
server/handle_server_request_email_confirmation.go
···
)
func (s *Server) handleServerRequestEmailConfirmation(e echo.Context) error {
urepo := e.Get("repo").(*models.RepoActor)
if urepo.EmailConfirmedAt != nil {
···
code := fmt.Sprintf("%s-%s", helpers.RandomVarchar(5), helpers.RandomVarchar(5))
eat := time.Now().Add(10 * time.Minute).UTC()
-
if err := s.db.Exec("UPDATE repos SET email_verification_code = ?, email_verification_code_expires_at = ? WHERE did = ?", nil, code, eat, urepo.Repo.Did).Error; err != nil {
-
s.logger.Error("error updating user", "error", err)
return helpers.ServerError(e, nil)
}
if err := s.sendEmailVerification(urepo.Email, urepo.Handle, code); err != nil {
-
s.logger.Error("error sending mail", "error", err)
return helpers.ServerError(e, nil)
}
···
)
func (s *Server) handleServerRequestEmailConfirmation(e echo.Context) error {
+
ctx := e.Request().Context()
+
logger := s.logger.With("name", "handleServerRequestEmailConfirm")
+
urepo := e.Get("repo").(*models.RepoActor)
if urepo.EmailConfirmedAt != nil {
···
code := fmt.Sprintf("%s-%s", helpers.RandomVarchar(5), helpers.RandomVarchar(5))
eat := time.Now().Add(10 * time.Minute).UTC()
+
if err := s.db.Exec(ctx, "UPDATE repos SET email_verification_code = ?, email_verification_code_expires_at = ? WHERE did = ?", nil, code, eat, urepo.Repo.Did).Error; err != nil {
+
logger.Error("error updating user", "error", err)
return helpers.ServerError(e, nil)
}
if err := s.sendEmailVerification(urepo.Email, urepo.Handle, code); err != nil {
+
logger.Error("error sending mail", "error", err)
return helpers.ServerError(e, nil)
}
+6 -3
server/handle_server_request_email_update.go
···
}
func (s *Server) handleServerRequestEmailUpdate(e echo.Context) error {
urepo := e.Get("repo").(*models.RepoActor)
if urepo.EmailConfirmedAt != nil {
code := fmt.Sprintf("%s-%s", helpers.RandomVarchar(5), helpers.RandomVarchar(5))
eat := time.Now().Add(10 * time.Minute).UTC()
-
if err := s.db.Exec("UPDATE repos SET email_update_code = ?, email_update_code_expires_at = ? WHERE did = ?", nil, code, eat, urepo.Repo.Did).Error; err != nil {
-
s.logger.Error("error updating repo", "error", err)
return helpers.ServerError(e, nil)
}
if err := s.sendEmailUpdate(urepo.Email, urepo.Handle, code); err != nil {
-
s.logger.Error("error sending email", "error", err)
return helpers.ServerError(e, nil)
}
}
···
}
func (s *Server) handleServerRequestEmailUpdate(e echo.Context) error {
+
ctx := e.Request().Context()
+
logger := s.logger.With("name", "handleServerRequestEmailUpdate")
+
urepo := e.Get("repo").(*models.RepoActor)
if urepo.EmailConfirmedAt != nil {
code := fmt.Sprintf("%s-%s", helpers.RandomVarchar(5), helpers.RandomVarchar(5))
eat := time.Now().Add(10 * time.Minute).UTC()
+
if err := s.db.Exec(ctx, "UPDATE repos SET email_update_code = ?, email_update_code_expires_at = ? WHERE did = ?", nil, code, eat, urepo.Repo.Did).Error; err != nil {
+
logger.Error("error updating repo", "error", err)
return helpers.ServerError(e, nil)
}
if err := s.sendEmailUpdate(urepo.Email, urepo.Handle, code); err != nil {
+
logger.Error("error sending email", "error", err)
return helpers.ServerError(e, nil)
}
}
+7 -4
server/handle_server_request_password_reset.go
···
}
func (s *Server) handleServerRequestPasswordReset(e echo.Context) error {
urepo, ok := e.Get("repo").(*models.RepoActor)
if !ok {
var req ComAtprotoServerRequestPasswordResetRequest
···
return err
}
-
murepo, err := s.getRepoActorByEmail(req.Email)
if err != nil {
return err
}
···
code := fmt.Sprintf("%s-%s", helpers.RandomVarchar(5), helpers.RandomVarchar(5))
eat := time.Now().Add(10 * time.Minute).UTC()
-
if err := s.db.Exec("UPDATE repos SET password_reset_code = ?, password_reset_code_expires_at = ? WHERE did = ?", nil, code, eat, urepo.Repo.Did).Error; err != nil {
-
s.logger.Error("error updating repo", "error", err)
return helpers.ServerError(e, nil)
}
if err := s.sendPasswordReset(urepo.Email, urepo.Handle, code); err != nil {
-
s.logger.Error("error sending email", "error", err)
return helpers.ServerError(e, nil)
}
···
}
func (s *Server) handleServerRequestPasswordReset(e echo.Context) error {
+
ctx := e.Request().Context()
+
logger := s.logger.With("name", "handleServerRequestPasswordReset")
+
urepo, ok := e.Get("repo").(*models.RepoActor)
if !ok {
var req ComAtprotoServerRequestPasswordResetRequest
···
return err
}
+
murepo, err := s.getRepoActorByEmail(ctx, req.Email)
if err != nil {
return err
}
···
code := fmt.Sprintf("%s-%s", helpers.RandomVarchar(5), helpers.RandomVarchar(5))
eat := time.Now().Add(10 * time.Minute).UTC()
+
if err := s.db.Exec(ctx, "UPDATE repos SET password_reset_code = ?, password_reset_code_expires_at = ? WHERE did = ?", nil, code, eat, urepo.Repo.Did).Error; err != nil {
+
logger.Error("error updating repo", "error", err)
return helpers.ServerError(e, nil)
}
if err := s.sendPasswordReset(urepo.Email, urepo.Handle, code); err != nil {
+
logger.Error("error sending email", "error", err)
return helpers.ServerError(e, nil)
}
+17 -13
server/handle_server_reserve_signing_key.go
···
package server
import (
"time"
"github.com/bluesky-social/indigo/atproto/atcrypto"
···
}
func (s *Server) handleServerReserveSigningKey(e echo.Context) error {
var req ServerReserveSigningKeyRequest
if err := e.Bind(&req); err != nil {
-
s.logger.Error("could not bind reserve signing key request", "error", err)
return helpers.ServerError(e, nil)
}
if req.Did != nil && *req.Did != "" {
var existing models.ReservedKey
-
if err := s.db.Raw("SELECT * FROM reserved_keys WHERE did = ?", nil, *req.Did).Scan(&existing).Error; err == nil && existing.KeyDid != "" {
return e.JSON(200, ServerReserveSigningKeyResponse{
SigningKey: existing.KeyDid,
})
···
k, err := atcrypto.GeneratePrivateKeyK256()
if err != nil {
-
s.logger.Error("error creating signing key", "endpoint", "com.atproto.server.reserveSigningKey", "error", err)
return helpers.ServerError(e, nil)
}
pubKey, err := k.PublicKey()
if err != nil {
-
s.logger.Error("error getting public key", "endpoint", "com.atproto.server.reserveSigningKey", "error", err)
return helpers.ServerError(e, nil)
}
···
CreatedAt: time.Now(),
}
-
if err := s.db.Create(&reservedKey, nil).Error; err != nil {
-
s.logger.Error("error storing reserved key", "endpoint", "com.atproto.server.reserveSigningKey", "error", err)
return helpers.ServerError(e, nil)
}
-
s.logger.Info("reserved signing key", "keyDid", keyDid, "forDid", req.Did)
return e.JSON(200, ServerReserveSigningKeyResponse{
SigningKey: keyDid,
})
}
-
func (s *Server) getReservedKey(keyDidOrDid string) (*models.ReservedKey, error) {
var reservedKey models.ReservedKey
-
if err := s.db.Raw("SELECT * FROM reserved_keys WHERE key_did = ?", nil, keyDidOrDid).Scan(&reservedKey).Error; err == nil && reservedKey.KeyDid != "" {
return &reservedKey, nil
}
-
if err := s.db.Raw("SELECT * FROM reserved_keys WHERE did = ?", nil, keyDidOrDid).Scan(&reservedKey).Error; err == nil && reservedKey.KeyDid != "" {
return &reservedKey, nil
}
return nil, nil
}
-
func (s *Server) deleteReservedKey(keyDid string, did *string) error {
-
if err := s.db.Exec("DELETE FROM reserved_keys WHERE key_did = ?", nil, keyDid).Error; err != nil {
return err
}
if did != nil && *did != "" {
-
if err := s.db.Exec("DELETE FROM reserved_keys WHERE did = ?", nil, *did).Error; err != nil {
return err
}
}
···
package server
import (
+
"context"
"time"
"github.com/bluesky-social/indigo/atproto/atcrypto"
···
}
func (s *Server) handleServerReserveSigningKey(e echo.Context) error {
+
ctx := e.Request().Context()
+
logger := s.logger.With("name", "handleServerReserveSigningKey")
+
var req ServerReserveSigningKeyRequest
if err := e.Bind(&req); err != nil {
+
logger.Error("could not bind reserve signing key request", "error", err)
return helpers.ServerError(e, nil)
}
if req.Did != nil && *req.Did != "" {
var existing models.ReservedKey
+
if err := s.db.Raw(ctx, "SELECT * FROM reserved_keys WHERE did = ?", nil, *req.Did).Scan(&existing).Error; err == nil && existing.KeyDid != "" {
return e.JSON(200, ServerReserveSigningKeyResponse{
SigningKey: existing.KeyDid,
})
···
k, err := atcrypto.GeneratePrivateKeyK256()
if err != nil {
+
logger.Error("error creating signing key", "endpoint", "com.atproto.server.reserveSigningKey", "error", err)
return helpers.ServerError(e, nil)
}
pubKey, err := k.PublicKey()
if err != nil {
+
logger.Error("error getting public key", "endpoint", "com.atproto.server.reserveSigningKey", "error", err)
return helpers.ServerError(e, nil)
}
···
CreatedAt: time.Now(),
}
+
if err := s.db.Create(ctx, &reservedKey, nil).Error; err != nil {
+
logger.Error("error storing reserved key", "endpoint", "com.atproto.server.reserveSigningKey", "error", err)
return helpers.ServerError(e, nil)
}
+
logger.Info("reserved signing key", "keyDid", keyDid, "forDid", req.Did)
return e.JSON(200, ServerReserveSigningKeyResponse{
SigningKey: keyDid,
})
}
+
func (s *Server) getReservedKey(ctx context.Context, keyDidOrDid string) (*models.ReservedKey, error) {
var reservedKey models.ReservedKey
+
if err := s.db.Raw(ctx, "SELECT * FROM reserved_keys WHERE key_did = ?", nil, keyDidOrDid).Scan(&reservedKey).Error; err == nil && reservedKey.KeyDid != "" {
return &reservedKey, nil
}
+
if err := s.db.Raw(ctx, "SELECT * FROM reserved_keys WHERE did = ?", nil, keyDidOrDid).Scan(&reservedKey).Error; err == nil && reservedKey.KeyDid != "" {
return &reservedKey, nil
}
return nil, nil
}
+
func (s *Server) deleteReservedKey(ctx context.Context, keyDid string, did *string) error {
+
if err := s.db.Exec(ctx, "DELETE FROM reserved_keys WHERE key_did = ?", nil, keyDid).Error; err != nil {
return err
}
if did != nil && *did != "" {
+
if err := s.db.Exec(ctx, "DELETE FROM reserved_keys WHERE did = ?", nil, *did).Error; err != nil {
return err
}
}
+7 -4
server/handle_server_reset_password.go
···
}
func (s *Server) handleServerResetPassword(e echo.Context) error {
urepo := e.Get("repo").(*models.RepoActor)
var req ComAtprotoServerResetPasswordRequest
if err := e.Bind(&req); err != nil {
-
s.logger.Error("error binding", "error", err)
return helpers.ServerError(e, nil)
}
···
hash, err := bcrypt.GenerateFromPassword([]byte(req.Password), 10)
if err != nil {
-
s.logger.Error("error creating hash", "error", err)
return helpers.ServerError(e, nil)
}
-
if err := s.db.Exec("UPDATE repos SET password_reset_code = NULL, password_reset_code_expires_at = NULL, password = ? WHERE did = ?", nil, hash, urepo.Repo.Did).Error; err != nil {
-
s.logger.Error("error updating repo", "error", err)
return helpers.ServerError(e, nil)
}
···
}
func (s *Server) handleServerResetPassword(e echo.Context) error {
+
ctx := e.Request().Context()
+
logger := s.logger.With("name", "handleServerResetPassword")
+
urepo := e.Get("repo").(*models.RepoActor)
var req ComAtprotoServerResetPasswordRequest
if err := e.Bind(&req); err != nil {
+
logger.Error("error binding", "error", err)
return helpers.ServerError(e, nil)
}
···
hash, err := bcrypt.GenerateFromPassword([]byte(req.Password), 10)
if err != nil {
+
logger.Error("error creating hash", "error", err)
return helpers.ServerError(e, nil)
}
+
if err := s.db.Exec(ctx, "UPDATE repos SET password_reset_code = NULL, password_reset_code_expires_at = NULL, password = ? WHERE did = ?", nil, hash, urepo.Repo.Did).Error; err != nil {
+
logger.Error("error updating repo", "error", err)
return helpers.ServerError(e, nil)
}
+3 -1
server/handle_server_resolve_handle.go
···
)
func (s *Server) handleResolveHandle(e echo.Context) error {
type Resp struct {
Did string `json:"did"`
}
···
ctx := context.WithValue(e.Request().Context(), "skip-cache", true)
did, err := s.passport.ResolveHandle(ctx, parsed.String())
if err != nil {
-
s.logger.Error("error resolving handle", "error", err)
return helpers.ServerError(e, nil)
}
···
)
func (s *Server) handleResolveHandle(e echo.Context) error {
+
logger := s.logger.With("name", "handleServerResolveHandle")
+
type Resp struct {
Did string `json:"did"`
}
···
ctx := context.WithValue(e.Request().Context(), "skip-cache", true)
did, err := s.passport.ResolveHandle(ctx, parsed.String())
if err != nil {
+
logger.Error("error resolving handle", "error", err)
return helpers.ServerError(e, nil)
}
+34 -9
server/handle_server_update_email.go
···
type ComAtprotoServerUpdateEmailRequest struct {
Email string `json:"email" validate:"required"`
EmailAuthFactor bool `json:"emailAuthFactor"`
-
Token string `json:"token" validate:"required"`
}
func (s *Server) handleServerUpdateEmail(e echo.Context) error {
urepo := e.Get("repo").(*models.RepoActor)
var req ComAtprotoServerUpdateEmailRequest
if err := e.Bind(&req); err != nil {
-
s.logger.Error("error binding", "error", err)
return helpers.ServerError(e, nil)
}
···
return helpers.InputError(e, nil)
}
-
if urepo.EmailUpdateCode == nil || urepo.EmailUpdateCodeExpiresAt == nil {
return helpers.InvalidTokenError(e)
}
-
if *urepo.EmailUpdateCode != req.Token {
-
return helpers.InvalidTokenError(e)
}
-
if time.Now().UTC().After(*urepo.EmailUpdateCodeExpiresAt) {
-
return helpers.ExpiredTokenError(e)
}
-
if err := s.db.Exec("UPDATE repos SET email_update_code = NULL, email_update_code_expires_at = NULL, email_confirmed_at = NULL, email = ? WHERE did = ?", nil, req.Email, urepo.Repo.Did).Error; err != nil {
-
s.logger.Error("error updating repo", "error", err)
return helpers.ServerError(e, nil)
}
···
type ComAtprotoServerUpdateEmailRequest struct {
Email string `json:"email" validate:"required"`
EmailAuthFactor bool `json:"emailAuthFactor"`
+
Token string `json:"token"`
}
func (s *Server) handleServerUpdateEmail(e echo.Context) error {
+
ctx := e.Request().Context()
+
logger := s.logger.With("name", "handleServerUpdateEmail")
+
urepo := e.Get("repo").(*models.RepoActor)
var req ComAtprotoServerUpdateEmailRequest
if err := e.Bind(&req); err != nil {
+
logger.Error("error binding", "error", err)
return helpers.ServerError(e, nil)
}
···
return helpers.InputError(e, nil)
}
+
// To disable email auth factor a token is required.
+
// To enable email auth factor a token is not required.
+
// If updating an email address, a token will be sent anyway
+
if urepo.TwoFactorType != models.TwoFactorTypeNone && req.EmailAuthFactor == false && req.Token == "" {
return helpers.InvalidTokenError(e)
}
+
if req.Token != "" {
+
if urepo.EmailUpdateCode == nil || urepo.EmailUpdateCodeExpiresAt == nil {
+
return helpers.InvalidTokenError(e)
+
}
+
+
if *urepo.EmailUpdateCode != req.Token {
+
return helpers.InvalidTokenError(e)
+
}
+
+
if time.Now().UTC().After(*urepo.EmailUpdateCodeExpiresAt) {
+
return helpers.ExpiredTokenError(e)
+
}
+
}
+
+
twoFactorType := models.TwoFactorTypeNone
+
if req.EmailAuthFactor {
+
twoFactorType = models.TwoFactorTypeEmail
}
+
query := "UPDATE repos SET email_update_code = NULL, email_update_code_expires_at = NULL, two_factor_type = ?, email = ?"
+
+
if urepo.Email != req.Email {
+
query += ",email_confirmed_at = NULL"
}
+
query += " WHERE did = ?"
+
+
if err := s.db.Exec(ctx, query, nil, twoFactorType, req.Email, urepo.Repo.Did).Error; err != nil {
+
logger.Error("error updating repo", "error", err)
return helpers.ServerError(e, nil)
}
+14 -11
server/handle_sync_get_blob.go
···
)
func (s *Server) handleSyncGetBlob(e echo.Context) error {
did := e.QueryParam("did")
if did == "" {
return helpers.InputError(e, nil)
···
return helpers.InputError(e, nil)
}
-
urepo, err := s.getRepoActorByDid(did)
if err != nil {
-
s.logger.Error("could not find user for requested blob", "error", err)
return helpers.InputError(e, nil)
}
···
}
var blob models.Blob
-
if err := s.db.Raw("SELECT * FROM blobs WHERE did = ? AND cid = ?", nil, did, c.Bytes()).Scan(&blob).Error; err != nil {
-
s.logger.Error("error looking up blob", "error", err)
return helpers.ServerError(e, nil)
}
···
if blob.Storage == "sqlite" {
var parts []models.BlobPart
-
if err := s.db.Raw("SELECT * FROM blob_parts WHERE blob_id = ? ORDER BY idx", nil, blob.ID).Scan(&parts).Error; err != nil {
-
s.logger.Error("error getting blob parts", "error", err)
return helpers.ServerError(e, nil)
}
···
}
} else if blob.Storage == "s3" {
if !(s.s3Config != nil && s.s3Config.BlobstoreEnabled) {
-
s.logger.Error("s3 storage disabled")
return helpers.ServerError(e, nil)
}
···
sess, err := session.NewSession(config)
if err != nil {
-
s.logger.Error("error creating aws session", "error", err)
return helpers.ServerError(e, nil)
}
···
Bucket: aws.String(s.s3Config.Bucket),
Key: aws.String(blobKey),
}); err != nil {
-
s.logger.Error("error getting blob from s3", "error", err)
return helpers.ServerError(e, nil)
} else {
read := 0
···
break
}
} else if err != nil && err != io.ErrUnexpectedEOF {
-
s.logger.Error("error reading blob", "error", err)
return helpers.ServerError(e, nil)
}
···
}
}
} else {
-
s.logger.Error("unknown storage", "storage", blob.Storage)
return helpers.ServerError(e, nil)
}
···
)
func (s *Server) handleSyncGetBlob(e echo.Context) error {
+
ctx := e.Request().Context()
+
logger := s.logger.With("name", "handleSyncGetBlob")
+
did := e.QueryParam("did")
if did == "" {
return helpers.InputError(e, nil)
···
return helpers.InputError(e, nil)
}
+
urepo, err := s.getRepoActorByDid(ctx, did)
if err != nil {
+
logger.Error("could not find user for requested blob", "error", err)
return helpers.InputError(e, nil)
}
···
}
var blob models.Blob
+
if err := s.db.Raw(ctx, "SELECT * FROM blobs WHERE did = ? AND cid = ?", nil, did, c.Bytes()).Scan(&blob).Error; err != nil {
+
logger.Error("error looking up blob", "error", err)
return helpers.ServerError(e, nil)
}
···
if blob.Storage == "sqlite" {
var parts []models.BlobPart
+
if err := s.db.Raw(ctx, "SELECT * FROM blob_parts WHERE blob_id = ? ORDER BY idx", nil, blob.ID).Scan(&parts).Error; err != nil {
+
logger.Error("error getting blob parts", "error", err)
return helpers.ServerError(e, nil)
}
···
}
} else if blob.Storage == "s3" {
if !(s.s3Config != nil && s.s3Config.BlobstoreEnabled) {
+
logger.Error("s3 storage disabled")
return helpers.ServerError(e, nil)
}
···
sess, err := session.NewSession(config)
if err != nil {
+
logger.Error("error creating aws session", "error", err)
return helpers.ServerError(e, nil)
}
···
Bucket: aws.String(s.s3Config.Bucket),
Key: aws.String(blobKey),
}); err != nil {
+
logger.Error("error getting blob from s3", "error", err)
return helpers.ServerError(e, nil)
} else {
read := 0
···
break
}
} else if err != nil && err != io.ErrUnexpectedEOF {
+
logger.Error("error reading blob", "error", err)
return helpers.ServerError(e, nil)
}
···
}
}
} else {
+
logger.Error("unknown storage", "storage", blob.Storage)
return helpers.ServerError(e, nil)
}
+3 -2
server/handle_sync_get_blocks.go
···
func (s *Server) handleGetBlocks(e echo.Context) error {
ctx := e.Request().Context()
var req ComAtprotoSyncGetBlocksRequest
if err := e.Bind(&req); err != nil {
···
cids = append(cids, c)
}
-
urepo, err := s.getRepoActorByDid(req.Did)
if err != nil {
return helpers.ServerError(e, nil)
}
···
})
if _, err := carstore.LdWrite(buf, hb); err != nil {
-
s.logger.Error("error writing to car", "error", err)
return helpers.ServerError(e, nil)
}
···
func (s *Server) handleGetBlocks(e echo.Context) error {
ctx := e.Request().Context()
+
logger := s.logger.With("name", "handleSyncGetBlocks")
var req ComAtprotoSyncGetBlocksRequest
if err := e.Bind(&req); err != nil {
···
cids = append(cids, c)
}
+
urepo, err := s.getRepoActorByDid(ctx, req.Did)
if err != nil {
return helpers.ServerError(e, nil)
}
···
})
if _, err := carstore.LdWrite(buf, hb); err != nil {
+
logger.Error("error writing to car", "error", err)
return helpers.ServerError(e, nil)
}
+3 -1
server/handle_sync_get_latest_commit.go
···
}
func (s *Server) handleSyncGetLatestCommit(e echo.Context) error {
did := e.QueryParam("did")
if did == "" {
return helpers.InputError(e, nil)
}
-
urepo, err := s.getRepoActorByDid(did)
if err != nil {
return err
}
···
}
func (s *Server) handleSyncGetLatestCommit(e echo.Context) error {
+
ctx := e.Request().Context()
+
did := e.QueryParam("did")
if did == "" {
return helpers.InputError(e, nil)
}
+
urepo, err := s.getRepoActorByDid(ctx, did)
if err != nil {
return err
}
+8 -5
server/handle_sync_get_record.go
···
)
func (s *Server) handleSyncGetRecord(e echo.Context) error {
did := e.QueryParam("did")
collection := e.QueryParam("collection")
rkey := e.QueryParam("rkey")
var urepo models.Repo
-
if err := s.db.Raw("SELECT * FROM repos WHERE did = ?", nil, did).Scan(&urepo).Error; err != nil {
-
s.logger.Error("error getting repo", "error", err)
return helpers.ServerError(e, nil)
}
-
root, blocks, err := s.repoman.getRecordProof(urepo, collection, rkey)
if err != nil {
return err
}
···
})
if _, err := carstore.LdWrite(buf, hb); err != nil {
-
s.logger.Error("error writing to car", "error", err)
return helpers.ServerError(e, nil)
}
for _, blk := range blocks {
if _, err := carstore.LdWrite(buf, blk.Cid().Bytes(), blk.RawData()); err != nil {
-
s.logger.Error("error writing to car", "error", err)
return helpers.ServerError(e, nil)
}
}
···
)
func (s *Server) handleSyncGetRecord(e echo.Context) error {
+
ctx := e.Request().Context()
+
logger := s.logger.With("name", "handleSyncGetRecord")
+
did := e.QueryParam("did")
collection := e.QueryParam("collection")
rkey := e.QueryParam("rkey")
var urepo models.Repo
+
if err := s.db.Raw(ctx, "SELECT * FROM repos WHERE did = ?", nil, did).Scan(&urepo).Error; err != nil {
+
logger.Error("error getting repo", "error", err)
return helpers.ServerError(e, nil)
}
+
root, blocks, err := s.repoman.getRecordProof(ctx, urepo, collection, rkey)
if err != nil {
return err
}
···
})
if _, err := carstore.LdWrite(buf, hb); err != nil {
+
logger.Error("error writing to car", "error", err)
return helpers.ServerError(e, nil)
}
for _, blk := range blocks {
if _, err := carstore.LdWrite(buf, blk.Cid().Bytes(), blk.RawData()); err != nil {
+
logger.Error("error writing to car", "error", err)
return helpers.ServerError(e, nil)
}
}
+6 -3
server/handle_sync_get_repo.go
···
)
func (s *Server) handleSyncGetRepo(e echo.Context) error {
did := e.QueryParam("did")
if did == "" {
return helpers.InputError(e, nil)
}
-
urepo, err := s.getRepoActorByDid(did)
if err != nil {
return err
}
···
buf := new(bytes.Buffer)
if _, err := carstore.LdWrite(buf, hb); err != nil {
-
s.logger.Error("error writing to car", "error", err)
return helpers.ServerError(e, nil)
}
var blocks []models.Block
-
if err := s.db.Raw("SELECT * FROM blocks WHERE did = ? ORDER BY rev ASC", nil, urepo.Repo.Did).Scan(&blocks).Error; err != nil {
return err
}
···
)
func (s *Server) handleSyncGetRepo(e echo.Context) error {
+
ctx := e.Request().Context()
+
logger := s.logger.With("name", "handleSyncGetRepo")
+
did := e.QueryParam("did")
if did == "" {
return helpers.InputError(e, nil)
}
+
urepo, err := s.getRepoActorByDid(ctx, did)
if err != nil {
return err
}
···
buf := new(bytes.Buffer)
if _, err := carstore.LdWrite(buf, hb); err != nil {
+
logger.Error("error writing to car", "error", err)
return helpers.ServerError(e, nil)
}
var blocks []models.Block
+
if err := s.db.Raw(ctx, "SELECT * FROM blocks WHERE did = ? ORDER BY rev ASC", nil, urepo.Repo.Did).Scan(&blocks).Error; err != nil {
return err
}
+3 -1
server/handle_sync_get_repo_status.go
···
// TODO: make this actually do the right thing
func (s *Server) handleSyncGetRepoStatus(e echo.Context) error {
did := e.QueryParam("did")
if did == "" {
return helpers.InputError(e, nil)
}
-
urepo, err := s.getRepoActorByDid(did)
if err != nil {
return err
}
···
// TODO: make this actually do the right thing
func (s *Server) handleSyncGetRepoStatus(e echo.Context) error {
+
ctx := e.Request().Context()
+
did := e.QueryParam("did")
if did == "" {
return helpers.InputError(e, nil)
}
+
urepo, err := s.getRepoActorByDid(ctx, did)
if err != nil {
return err
}
+8 -5
server/handle_sync_list_blobs.go
···
}
func (s *Server) handleSyncListBlobs(e echo.Context) error {
did := e.QueryParam("did")
if did == "" {
return helpers.InputError(e, nil)
···
}
params = append(params, limit)
-
urepo, err := s.getRepoActorByDid(did)
if err != nil {
-
s.logger.Error("could not find user for requested blobs", "error", err)
return helpers.InputError(e, nil)
}
···
}
var blobs []models.Blob
-
if err := s.db.Raw("SELECT * FROM blobs WHERE did = ? "+cursorquery+" ORDER BY created_at DESC LIMIT ?", nil, params...).Scan(&blobs).Error; err != nil {
-
s.logger.Error("error getting records", "error", err)
return helpers.ServerError(e, nil)
}
···
for _, b := range blobs {
c, err := cid.Cast(b.Cid)
if err != nil {
-
s.logger.Error("error casting cid", "error", err)
return helpers.ServerError(e, nil)
}
cstrs = append(cstrs, c.String())
···
}
func (s *Server) handleSyncListBlobs(e echo.Context) error {
+
ctx := e.Request().Context()
+
logger := s.logger.With("name", "handleSyncListBlobs")
+
did := e.QueryParam("did")
if did == "" {
return helpers.InputError(e, nil)
···
}
params = append(params, limit)
+
urepo, err := s.getRepoActorByDid(ctx, did)
if err != nil {
+
logger.Error("could not find user for requested blobs", "error", err)
return helpers.InputError(e, nil)
}
···
}
var blobs []models.Blob
+
if err := s.db.Raw(ctx, "SELECT * FROM blobs WHERE did = ? "+cursorquery+" ORDER BY created_at DESC LIMIT ?", nil, params...).Scan(&blobs).Error; err != nil {
+
logger.Error("error getting records", "error", err)
return helpers.ServerError(e, nil)
}
···
for _, b := range blobs {
c, err := cid.Cast(b.Cid)
if err != nil {
+
logger.Error("error casting cid", "error", err)
return helpers.ServerError(e, nil)
}
cstrs = append(cstrs, c.String())
+82 -50
server/handle_sync_subscribe_repos.go
···
"github.com/bluesky-social/indigo/events"
"github.com/bluesky-social/indigo/lex/util"
"github.com/btcsuite/websocket"
"github.com/labstack/echo/v4"
)
func (s *Server) handleSyncSubscribeRepos(e echo.Context) error {
-
ctx := e.Request().Context()
logger := s.logger.With("component", "subscribe-repos-websocket")
conn, err := websocket.Upgrade(e.Response().Writer, e.Request(), e.Response().Header(), 1<<10, 1<<10)
···
logger = logger.With("ident", ident)
logger.Info("new connection established")
-
evts, cancel, err := s.evtman.Subscribe(ctx, ident, func(evt *events.XRPCStreamEvent) bool {
return true
}, nil)
if err != nil {
return err
}
-
defer cancel()
header := events.EventHeader{Op: events.EvtKindMessage}
for evt := range evts {
-
wc, err := conn.NextWriter(websocket.BinaryMessage)
-
if err != nil {
-
logger.Error("error writing message to relay", "err", err)
-
break
-
}
-
if ctx.Err() != nil {
-
logger.Error("context error", "err", err)
-
break
-
}
-
var obj util.CBOR
-
switch {
-
case evt.Error != nil:
-
header.Op = events.EvtKindErrorFrame
-
obj = evt.Error
-
case evt.RepoCommit != nil:
-
header.MsgType = "#commit"
-
obj = evt.RepoCommit
-
case evt.RepoIdentity != nil:
-
header.MsgType = "#identity"
-
obj = evt.RepoIdentity
-
case evt.RepoAccount != nil:
-
header.MsgType = "#account"
-
obj = evt.RepoAccount
-
case evt.RepoInfo != nil:
-
header.MsgType = "#info"
-
obj = evt.RepoInfo
-
default:
-
logger.Warn("unrecognized event kind")
-
return nil
-
}
-
if err := header.MarshalCBOR(wc); err != nil {
-
logger.Error("failed to write header to relay", "err", err)
-
break
-
}
-
if err := obj.MarshalCBOR(wc); err != nil {
-
logger.Error("failed to write event to relay", "err", err)
-
break
-
}
-
if err := wc.Close(); err != nil {
-
logger.Error("failed to flush-close our event write", "err", err)
-
break
-
}
}
// we should tell the relay to request a new crawl at this point if we got disconnected
// use a new context since the old one might be cancelled at this point
-
ctx, cancel = context.WithTimeout(context.Background(), 10*time.Second)
-
defer cancel()
-
if err := s.requestCrawl(ctx); err != nil {
-
logger.Error("error requesting crawls", "err", err)
-
}
return nil
}
···
"github.com/bluesky-social/indigo/events"
"github.com/bluesky-social/indigo/lex/util"
"github.com/btcsuite/websocket"
+
"github.com/haileyok/cocoon/metrics"
"github.com/labstack/echo/v4"
)
func (s *Server) handleSyncSubscribeRepos(e echo.Context) error {
+
ctx, cancel := context.WithCancel(e.Request().Context())
+
defer cancel()
+
logger := s.logger.With("component", "subscribe-repos-websocket")
conn, err := websocket.Upgrade(e.Response().Writer, e.Request(), e.Response().Header(), 1<<10, 1<<10)
···
logger = logger.With("ident", ident)
logger.Info("new connection established")
+
metrics.RelaysConnected.WithLabelValues(ident).Inc()
+
defer func() {
+
metrics.RelaysConnected.WithLabelValues(ident).Dec()
+
}()
+
+
evts, evtManCancel, err := s.evtman.Subscribe(ctx, ident, func(evt *events.XRPCStreamEvent) bool {
return true
}, nil)
if err != nil {
return err
}
+
defer evtManCancel()
+
+
// drop the connection whenever a subscriber disconnects from the socket, we should get errors
+
go func() {
+
for {
+
select {
+
case <-ctx.Done():
+
return
+
default:
+
if _, _, err := conn.ReadMessage(); err != nil {
+
logger.Warn("websocket error", "err", err)
+
cancel()
+
return
+
}
+
}
+
}
+
}()
header := events.EventHeader{Op: events.EvtKindMessage}
for evt := range evts {
+
func() {
+
defer func() {
+
metrics.RelaySends.WithLabelValues(ident, header.MsgType).Inc()
+
}()
+
wc, err := conn.NextWriter(websocket.BinaryMessage)
+
if err != nil {
+
logger.Error("error writing message to relay", "err", err)
+
return
+
}
+
if ctx.Err() != nil {
+
logger.Error("context error", "err", err)
+
return
+
}
+
var obj util.CBOR
+
switch {
+
case evt.Error != nil:
+
header.Op = events.EvtKindErrorFrame
+
obj = evt.Error
+
case evt.RepoCommit != nil:
+
header.MsgType = "#commit"
+
obj = evt.RepoCommit
+
case evt.RepoIdentity != nil:
+
header.MsgType = "#identity"
+
obj = evt.RepoIdentity
+
case evt.RepoAccount != nil:
+
header.MsgType = "#account"
+
obj = evt.RepoAccount
+
case evt.RepoInfo != nil:
+
header.MsgType = "#info"
+
obj = evt.RepoInfo
+
default:
+
logger.Warn("unrecognized event kind")
+
return
+
}
+
if err := header.MarshalCBOR(wc); err != nil {
+
logger.Error("failed to write header to relay", "err", err)
+
return
+
}
+
+
if err := obj.MarshalCBOR(wc); err != nil {
+
logger.Error("failed to write event to relay", "err", err)
+
return
+
}
+
if err := wc.Close(); err != nil {
+
logger.Error("failed to flush-close our event write", "err", err)
+
return
+
}
+
}()
}
// we should tell the relay to request a new crawl at this point if we got disconnected
// use a new context since the old one might be cancelled at this point
+
go func() {
+
retryCtx, retryCancel := context.WithTimeout(context.Background(), 10*time.Second)
+
defer retryCancel()
+
if err := s.requestCrawl(retryCtx); err != nil {
+
logger.Error("error requesting crawls", "err", err)
+
}
+
}()
return nil
}
+5 -2
server/handle_well_known.go
···
}
func (s *Server) handleAtprotoDid(e echo.Context) error {
host := e.Request().Host
if host == "" {
return helpers.InputError(e, to.StringPtr("Invalid handle."))
···
return e.NoContent(404)
}
-
actor, err := s.getActorByHandle(host)
if err != nil {
if err == gorm.ErrRecordNotFound {
return e.NoContent(404)
}
-
s.logger.Error("error looking up actor by handle", "error", err)
return helpers.ServerError(e, nil)
}
···
}
func (s *Server) handleAtprotoDid(e echo.Context) error {
+
ctx := e.Request().Context()
+
logger := s.logger.With("name", "handleAtprotoDid")
+
host := e.Request().Host
if host == "" {
return helpers.InputError(e, to.StringPtr("Invalid handle."))
···
return e.NoContent(404)
}
+
actor, err := s.getActorByHandle(ctx, host)
if err != nil {
if err == gorm.ErrRecordNotFound {
return e.NoContent(404)
}
+
logger.Error("error looking up actor by handle", "error", err)
return helpers.ServerError(e, nil)
}
+19
server/mail.go
···
return nil
}
···
return nil
}
+
+
func (s *Server) sendTwoFactorCode(email, handle, code string) error {
+
if s.mail == nil {
+
return nil
+
}
+
+
s.mailLk.Lock()
+
defer s.mailLk.Unlock()
+
+
s.mail.To(email)
+
s.mail.Subject("2FA code for " + s.config.Hostname)
+
s.mail.Plain().Set(fmt.Sprintf("Hello %s. Your 2FA code is %s. This code will expire in ten minutes.", handle, code))
+
+
if err := s.mail.Send(); err != nil {
+
return err
+
}
+
+
return nil
+
}
+42 -21
server/middleware.go
···
func (s *Server) handleLegacySessionMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
return func(e echo.Context) error {
authheader := e.Request().Header.Get("authorization")
if authheader == "" {
return e.JSON(401, map[string]string{"error": "Unauthorized"})
···
if hasLxm {
pts := strings.Split(e.Request().URL.String(), "/")
if lxm != pts[len(pts)-1] {
-
s.logger.Error("service auth lxm incorrect", "lxm", lxm, "expected", pts[len(pts)-1], "error", err)
return helpers.InputError(e, nil)
}
maybeDid, ok := claims["iss"].(string)
if !ok {
-
s.logger.Error("no iss in service auth token", "error", err)
return helpers.InputError(e, nil)
}
did = maybeDid
-
maybeRepo, err := s.getRepoActorByDid(did)
if err != nil {
-
s.logger.Error("error fetching repo", "error", err)
return helpers.ServerError(e, nil)
}
repo = maybeRepo
···
return s.privateKey.Public(), nil
})
if err != nil {
-
s.logger.Error("error parsing jwt", "error", err)
return helpers.ExpiredTokenError(e)
}
···
hash := sha256.Sum256([]byte(signingInput))
sigBytes, err := base64.RawURLEncoding.DecodeString(kpts[2])
if err != nil {
-
s.logger.Error("error decoding signature bytes", "error", err)
return helpers.ServerError(e, nil)
}
if len(sigBytes) != 64 {
-
s.logger.Error("incorrect sigbytes length", "length", len(sigBytes))
return helpers.ServerError(e, nil)
}
···
sBytes := sigBytes[32:]
rr, _ := secp256k1.NewScalarFromBytes((*[32]byte)(rBytes))
ss, _ := secp256k1.NewScalarFromBytes((*[32]byte)(sBytes))
sk, err := secp256k1secec.NewPrivateKey(repo.SigningKey)
if err != nil {
-
s.logger.Error("can't load private key", "error", err)
return err
}
pubKey, ok := sk.Public().(*secp256k1secec.PublicKey)
if !ok {
-
s.logger.Error("error getting public key from sk")
return helpers.ServerError(e, nil)
}
verified := pubKey.VerifyRaw(hash[:], rr, ss)
if !verified {
-
s.logger.Error("error verifying", "error", err)
return helpers.ServerError(e, nil)
}
}
···
Found bool
}
var result Result
-
if err := s.db.Raw("SELECT EXISTS(SELECT 1 FROM "+table+" WHERE token = ?) AS found", nil, tokenstr).Scan(&result).Error; err != nil {
if err == gorm.ErrRecordNotFound {
return helpers.InvalidTokenError(e)
}
-
s.logger.Error("error getting token from db", "error", err)
return helpers.ServerError(e, nil)
}
···
exp, ok := claims["exp"].(float64)
if !ok {
-
s.logger.Error("error getting iat from token")
return helpers.ServerError(e, nil)
}
···
}
if repo == nil {
-
maybeRepo, err := s.getRepoActorByDid(claims["sub"].(string))
if err != nil {
-
s.logger.Error("error fetching repo", "error", err)
return helpers.ServerError(e, nil)
}
repo = maybeRepo
···
func (s *Server) handleOauthSessionMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
return func(e echo.Context) error {
authheader := e.Request().Header.Get("authorization")
if authheader == "" {
return e.JSON(401, map[string]string{"error": "Unauthorized"})
···
"error": "use_dpop_nonce",
})
}
-
s.logger.Error("invalid dpop proof", "error", err)
return helpers.InputError(e, nil)
}
var oauthToken provider.OauthToken
-
if err := s.db.Raw("SELECT * FROM oauth_tokens WHERE token = ?", nil, accessToken).Scan(&oauthToken).Error; err != nil {
-
s.logger.Error("error finding access token in db", "error", err)
return helpers.InputError(e, nil)
}
···
}
if *oauthToken.Parameters.DpopJkt != proof.JKT {
-
s.logger.Error("jkt mismatch", "token", oauthToken.Parameters.DpopJkt, "proof", proof.JKT)
return helpers.InputError(e, to.StringPtr("dpop jkt mismatch"))
}
···
})
}
-
repo, err := s.getRepoActorByDid(oauthToken.Sub)
if err != nil {
-
s.logger.Error("could not find actor in db", "error", err)
return helpers.ServerError(e, nil)
}
···
func (s *Server) handleLegacySessionMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
return func(e echo.Context) error {
+
ctx := e.Request().Context()
+
logger := s.logger.With("name", "handleLegacySessionMiddleware")
+
authheader := e.Request().Header.Get("authorization")
if authheader == "" {
return e.JSON(401, map[string]string{"error": "Unauthorized"})
···
if hasLxm {
pts := strings.Split(e.Request().URL.String(), "/")
if lxm != pts[len(pts)-1] {
+
logger.Error("service auth lxm incorrect", "lxm", lxm, "expected", pts[len(pts)-1], "error", err)
return helpers.InputError(e, nil)
}
maybeDid, ok := claims["iss"].(string)
if !ok {
+
logger.Error("no iss in service auth token", "error", err)
return helpers.InputError(e, nil)
}
did = maybeDid
+
maybeRepo, err := s.getRepoActorByDid(ctx, did)
if err != nil {
+
logger.Error("error fetching repo", "error", err)
return helpers.ServerError(e, nil)
}
repo = maybeRepo
···
return s.privateKey.Public(), nil
})
if err != nil {
+
logger.Error("error parsing jwt", "error", err)
return helpers.ExpiredTokenError(e)
}
···
hash := sha256.Sum256([]byte(signingInput))
sigBytes, err := base64.RawURLEncoding.DecodeString(kpts[2])
if err != nil {
+
logger.Error("error decoding signature bytes", "error", err)
return helpers.ServerError(e, nil)
}
if len(sigBytes) != 64 {
+
logger.Error("incorrect sigbytes length", "length", len(sigBytes))
return helpers.ServerError(e, nil)
}
···
sBytes := sigBytes[32:]
rr, _ := secp256k1.NewScalarFromBytes((*[32]byte)(rBytes))
ss, _ := secp256k1.NewScalarFromBytes((*[32]byte)(sBytes))
+
+
if repo == nil {
+
sub, ok := claims["sub"].(string)
+
if !ok {
+
s.logger.Error("no sub claim in ES256K token and repo not set")
+
return helpers.InvalidTokenError(e)
+
}
+
maybeRepo, err := s.getRepoActorByDid(ctx, sub)
+
if err != nil {
+
s.logger.Error("error fetching repo for ES256K verification", "error", err)
+
return helpers.ServerError(e, nil)
+
}
+
repo = maybeRepo
+
did = sub
+
}
sk, err := secp256k1secec.NewPrivateKey(repo.SigningKey)
if err != nil {
+
logger.Error("can't load private key", "error", err)
return err
}
pubKey, ok := sk.Public().(*secp256k1secec.PublicKey)
if !ok {
+
logger.Error("error getting public key from sk")
return helpers.ServerError(e, nil)
}
verified := pubKey.VerifyRaw(hash[:], rr, ss)
if !verified {
+
logger.Error("error verifying", "error", err)
return helpers.ServerError(e, nil)
}
}
···
Found bool
}
var result Result
+
if err := s.db.Raw(ctx, "SELECT EXISTS(SELECT 1 FROM "+table+" WHERE token = ?) AS found", nil, tokenstr).Scan(&result).Error; err != nil {
if err == gorm.ErrRecordNotFound {
return helpers.InvalidTokenError(e)
}
+
logger.Error("error getting token from db", "error", err)
return helpers.ServerError(e, nil)
}
···
exp, ok := claims["exp"].(float64)
if !ok {
+
logger.Error("error getting iat from token")
return helpers.ServerError(e, nil)
}
···
}
if repo == nil {
+
maybeRepo, err := s.getRepoActorByDid(ctx, claims["sub"].(string))
if err != nil {
+
logger.Error("error fetching repo", "error", err)
return helpers.ServerError(e, nil)
}
repo = maybeRepo
···
func (s *Server) handleOauthSessionMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
return func(e echo.Context) error {
+
ctx := e.Request().Context()
+
logger := s.logger.With("name", "handleOauthSessionMiddleware")
+
authheader := e.Request().Header.Get("authorization")
if authheader == "" {
return e.JSON(401, map[string]string{"error": "Unauthorized"})
···
"error": "use_dpop_nonce",
})
}
+
logger.Error("invalid dpop proof", "error", err)
return helpers.InputError(e, nil)
}
var oauthToken provider.OauthToken
+
if err := s.db.Raw(ctx, "SELECT * FROM oauth_tokens WHERE token = ?", nil, accessToken).Scan(&oauthToken).Error; err != nil {
+
logger.Error("error finding access token in db", "error", err)
return helpers.InputError(e, nil)
}
···
}
if *oauthToken.Parameters.DpopJkt != proof.JKT {
+
logger.Error("jkt mismatch", "token", oauthToken.Parameters.DpopJkt, "proof", proof.JKT)
return helpers.InputError(e, to.StringPtr("dpop jkt mismatch"))
}
···
})
}
+
repo, err := s.getRepoActorByDid(ctx, oauthToken.Sub)
if err != nil {
+
logger.Error("could not find actor in db", "error", err)
return helpers.ServerError(e, nil)
}
+85 -32
server/repo.go
···
lexutil "github.com/bluesky-social/indigo/lex/util"
"github.com/bluesky-social/indigo/repo"
"github.com/haileyok/cocoon/internal/db"
"github.com/haileyok/cocoon/models"
"github.com/haileyok/cocoon/recording_blockstore"
blocks "github.com/ipfs/go-block-format"
···
}
// TODO make use of swap commit
-
func (rm *RepoMan) applyWrites(urepo models.Repo, writes []Op, swapCommit *string) ([]ApplyWriteResult, error) {
rootcid, err := cid.Cast(urepo.Root)
if err != nil {
return nil, err
···
dbs := rm.s.getBlockstore(urepo.Did)
bs := recording_blockstore.New(dbs)
-
r, err := repo.OpenRepo(context.TODO(), bs, rootcid)
-
entries := []models.Record{}
var results []ApplyWriteResult
for i, op := range writes {
if op.Type != OpTypeCreate && op.Rkey == nil {
return nil, fmt.Errorf("invalid rkey")
} else if op.Type == OpTypeCreate && op.Rkey != nil {
-
_, _, err := r.GetRecord(context.TODO(), op.Collection+"/"+*op.Rkey)
if err == nil {
op.Type = OpTypeUpdate
}
} else if op.Rkey == nil {
op.Rkey = to.StringPtr(rm.clock.Next().String())
writes[i].Rkey = op.Rkey
}
_, err := syntax.ParseRecordKey(*op.Rkey)
if err != nil {
return nil, err
···
switch op.Type {
case OpTypeCreate:
-
j, err := json.Marshal(*op.Record)
if err != nil {
return nil, err
}
-
out, err := atdata.UnmarshalJSON(j)
if err != nil {
return nil, err
}
mm := MarshalableMap(out)
// HACK: if a record doesn't contain a $type, we can manually set it here based on the op's collection
if mm["$type"] == "" {
mm["$type"] = op.Collection
}
-
nc, err := r.PutRecord(context.TODO(), op.Collection+"/"+*op.Rkey, &mm)
if err != nil {
return nil, err
}
d, err := atdata.MarshalCBOR(mm)
if err != nil {
return nil, err
}
entries = append(entries, models.Record{
Did: urepo.Did,
CreatedAt: rm.clock.Next().String(),
···
Cid: nc.String(),
Value: d,
})
results = append(results, ApplyWriteResult{
Type: to.StringPtr(OpTypeCreate.String()),
Uri: to.StringPtr("at://" + urepo.Did + "/" + op.Collection + "/" + *op.Rkey),
···
ValidationStatus: to.StringPtr("valid"), // TODO: obviously this might not be true atm lol
})
case OpTypeDelete:
var old models.Record
-
if err := rm.db.Raw("SELECT value FROM records WHERE did = ? AND nsid = ? AND rkey = ?", nil, urepo.Did, op.Collection, op.Rkey).Scan(&old).Error; err != nil {
return nil, err
}
entries = append(entries, models.Record{
Did: urepo.Did,
Nsid: op.Collection,
Rkey: *op.Rkey,
Value: old.Value,
})
-
err := r.DeleteRecord(context.TODO(), op.Collection+"/"+*op.Rkey)
if err != nil {
return nil, err
}
results = append(results, ApplyWriteResult{
Type: to.StringPtr(OpTypeDelete.String()),
})
case OpTypeUpdate:
-
j, err := json.Marshal(*op.Record)
if err != nil {
return nil, err
}
-
out, err := atdata.UnmarshalJSON(j)
if err != nil {
return nil, err
}
mm := MarshalableMap(out)
-
nc, err := r.UpdateRecord(context.TODO(), op.Collection+"/"+*op.Rkey, &mm)
if err != nil {
return nil, err
}
d, err := atdata.MarshalCBOR(mm)
if err != nil {
return nil, err
}
entries = append(entries, models.Record{
Did: urepo.Did,
CreatedAt: rm.clock.Next().String(),
···
Cid: nc.String(),
Value: d,
})
results = append(results, ApplyWriteResult{
Type: to.StringPtr(OpTypeUpdate.String()),
Uri: to.StringPtr("at://" + urepo.Did + "/" + op.Collection + "/" + *op.Rkey),
···
}
}
-
newroot, rev, err := r.Commit(context.TODO(), urepo.SignFor)
if err != nil {
return nil, err
}
buf := new(bytes.Buffer)
hb, err := cbor.DumpObject(&car.CarHeader{
Roots: []cid.Cid{newroot},
Version: 1,
})
-
if _, err := carstore.LdWrite(buf, hb); err != nil {
return nil, err
}
-
diffops, err := r.DiffSince(context.TODO(), rootcid)
if err != nil {
return nil, err
}
ops := make([]*atproto.SyncSubscribeRepos_RepoOp, 0, len(diffops))
-
for _, op := range diffops {
var c cid.Cid
switch op.Op {
···
})
}
-
blk, err := dbs.Get(context.TODO(), c)
if err != nil {
return nil, err
}
if _, err := carstore.LdWrite(buf, blk.Cid().Bytes(), blk.RawData()); err != nil {
return nil, err
}
}
for _, op := range bs.GetWriteLog() {
if _, err := carstore.LdWrite(buf, op.Cid().Bytes(), op.RawData()); err != nil {
return nil, err
}
}
var blobs []lexutil.LexLink
for _, entry := range entries {
var cids []cid.Cid
if entry.Cid != "" {
-
if err := rm.s.db.Create(&entry, []clause.Expression{clause.OnConflict{
Columns: []clause.Column{{Name: "did"}, {Name: "nsid"}, {Name: "rkey"}},
UpdateAll: true,
}}).Error; err != nil {
return nil, err
}
-
cids, err = rm.incrementBlobRefs(urepo, entry.Value)
if err != nil {
return nil, err
}
} else {
-
if err := rm.s.db.Delete(&entry, nil).Error; err != nil {
return nil, err
}
-
cids, err = rm.decrementBlobRefs(urepo, entry.Value)
if err != nil {
return nil, err
}
}
for _, c := range cids {
blobs = append(blobs, lexutil.LexLink(c))
}
}
-
rm.s.evtman.AddEvent(context.TODO(), &events.XRPCStreamEvent{
RepoCommit: &atproto.SyncSubscribeRepos_Commit{
Repo: urepo.Did,
Blocks: buf.Bytes(),
···
},
})
-
if err := rm.s.UpdateRepo(context.TODO(), urepo.Did, newroot, rev); err != nil {
return nil, err
}
···
return results, nil
}
-
func (rm *RepoMan) getRecordProof(urepo models.Repo, collection, rkey string) (cid.Cid, []blocks.Block, error) {
c, err := cid.Cast(urepo.Root)
if err != nil {
return cid.Undef, nil, err
···
dbs := rm.s.getBlockstore(urepo.Did)
bs := recording_blockstore.New(dbs)
-
r, err := repo.OpenRepo(context.TODO(), bs, c)
if err != nil {
return cid.Undef, nil, err
}
-
_, _, err = r.GetRecordBytes(context.TODO(), collection+"/"+rkey)
if err != nil {
return cid.Undef, nil, err
}
···
return c, bs.GetReadLog(), nil
}
-
func (rm *RepoMan) incrementBlobRefs(urepo models.Repo, cbor []byte) ([]cid.Cid, error) {
cids, err := getBlobCidsFromCbor(cbor)
if err != nil {
return nil, err
}
for _, c := range cids {
-
if err := rm.db.Exec("UPDATE blobs SET ref_count = ref_count + 1 WHERE did = ? AND cid = ?", nil, urepo.Did, c.Bytes()).Error; err != nil {
return nil, err
}
}
···
return cids, nil
}
-
func (rm *RepoMan) decrementBlobRefs(urepo models.Repo, cbor []byte) ([]cid.Cid, error) {
cids, err := getBlobCidsFromCbor(cbor)
if err != nil {
return nil, err
···
ID uint
Count int
}
-
if err := rm.db.Raw("UPDATE blobs SET ref_count = ref_count - 1 WHERE did = ? AND cid = ? RETURNING id, ref_count", nil, urepo.Did, c.Bytes()).Scan(&res).Error; err != nil {
return nil, err
}
if res.Count == 0 {
-
if err := rm.db.Exec("DELETE FROM blobs WHERE id = ?", nil, res.ID).Error; err != nil {
return nil, err
}
-
if err := rm.db.Exec("DELETE FROM blob_parts WHERE blob_id = ?", nil, res.ID).Error; err != nil {
return nil, err
}
}
···
lexutil "github.com/bluesky-social/indigo/lex/util"
"github.com/bluesky-social/indigo/repo"
"github.com/haileyok/cocoon/internal/db"
+
"github.com/haileyok/cocoon/metrics"
"github.com/haileyok/cocoon/models"
"github.com/haileyok/cocoon/recording_blockstore"
blocks "github.com/ipfs/go-block-format"
···
}
// TODO make use of swap commit
+
func (rm *RepoMan) applyWrites(ctx context.Context, urepo models.Repo, writes []Op, swapCommit *string) ([]ApplyWriteResult, error) {
rootcid, err := cid.Cast(urepo.Root)
if err != nil {
return nil, err
···
dbs := rm.s.getBlockstore(urepo.Did)
bs := recording_blockstore.New(dbs)
+
r, err := repo.OpenRepo(ctx, bs, rootcid)
var results []ApplyWriteResult
+
entries := make([]models.Record, 0, len(writes))
for i, op := range writes {
+
// updates or deletes must supply an rkey
if op.Type != OpTypeCreate && op.Rkey == nil {
return nil, fmt.Errorf("invalid rkey")
} else if op.Type == OpTypeCreate && op.Rkey != nil {
+
// we should conver this op to an update if the rkey already exists
+
_, _, err := r.GetRecord(ctx, fmt.Sprintf("%s/%s", op.Collection, *op.Rkey))
if err == nil {
op.Type = OpTypeUpdate
}
} else if op.Rkey == nil {
+
// creates that don't supply an rkey will have one generated for them
op.Rkey = to.StringPtr(rm.clock.Next().String())
writes[i].Rkey = op.Rkey
}
+
// validate the record key is actually valid
_, err := syntax.ParseRecordKey(*op.Rkey)
if err != nil {
return nil, err
···
switch op.Type {
case OpTypeCreate:
+
// HACK: this fixes some type conversions, mainly around integers
+
// first we convert to json bytes
+
b, err := json.Marshal(*op.Record)
if err != nil {
return nil, err
}
+
// then we use atdata.UnmarshalJSON to convert it back to a map
+
out, err := atdata.UnmarshalJSON(b)
if err != nil {
return nil, err
}
+
// finally we can cast to a MarshalableMap
mm := MarshalableMap(out)
// HACK: if a record doesn't contain a $type, we can manually set it here based on the op's collection
+
// i forget why this is actually necessary?
if mm["$type"] == "" {
mm["$type"] = op.Collection
}
+
nc, err := r.PutRecord(ctx, fmt.Sprintf("%s/%s", op.Collection, *op.Rkey), &mm)
if err != nil {
return nil, err
}
+
d, err := atdata.MarshalCBOR(mm)
if err != nil {
return nil, err
}
+
entries = append(entries, models.Record{
Did: urepo.Did,
CreatedAt: rm.clock.Next().String(),
···
Cid: nc.String(),
Value: d,
})
+
results = append(results, ApplyWriteResult{
Type: to.StringPtr(OpTypeCreate.String()),
Uri: to.StringPtr("at://" + urepo.Did + "/" + op.Collection + "/" + *op.Rkey),
···
ValidationStatus: to.StringPtr("valid"), // TODO: obviously this might not be true atm lol
})
case OpTypeDelete:
+
// try to find the old record in the database
var old models.Record
+
if err := rm.db.Raw(ctx, "SELECT value FROM records WHERE did = ? AND nsid = ? AND rkey = ?", nil, urepo.Did, op.Collection, op.Rkey).Scan(&old).Error; err != nil {
return nil, err
}
+
+
// TODO: this is really confusing, and looking at it i have no idea why i did this. below when we are doing deletes, we
+
// check if `cid` here is nil to indicate if we should delete. that really doesn't make much sense and its super illogical
+
// when reading this code. i dont feel like fixing right now though so
entries = append(entries, models.Record{
Did: urepo.Did,
Nsid: op.Collection,
Rkey: *op.Rkey,
Value: old.Value,
})
+
+
// delete the record from the repo
+
err := r.DeleteRecord(ctx, fmt.Sprintf("%s/%s", op.Collection, *op.Rkey))
if err != nil {
return nil, err
}
+
+
// add a result for the delete
results = append(results, ApplyWriteResult{
Type: to.StringPtr(OpTypeDelete.String()),
})
case OpTypeUpdate:
+
// HACK: same hack as above for type fixes
+
b, err := json.Marshal(*op.Record)
if err != nil {
return nil, err
}
+
out, err := atdata.UnmarshalJSON(b)
if err != nil {
return nil, err
}
mm := MarshalableMap(out)
+
+
nc, err := r.UpdateRecord(ctx, fmt.Sprintf("%s/%s", op.Collection, *op.Rkey), &mm)
if err != nil {
return nil, err
}
+
d, err := atdata.MarshalCBOR(mm)
if err != nil {
return nil, err
}
+
entries = append(entries, models.Record{
Did: urepo.Did,
CreatedAt: rm.clock.Next().String(),
···
Cid: nc.String(),
Value: d,
})
+
results = append(results, ApplyWriteResult{
Type: to.StringPtr(OpTypeUpdate.String()),
Uri: to.StringPtr("at://" + urepo.Did + "/" + op.Collection + "/" + *op.Rkey),
···
}
}
+
// commit and get the new root
+
newroot, rev, err := r.Commit(ctx, urepo.SignFor)
if err != nil {
return nil, err
}
+
for _, result := range results {
+
if result.Type != nil {
+
metrics.RepoOperations.WithLabelValues(*result.Type).Inc()
+
}
+
}
+
+
// create a buffer for dumping our new cbor into
buf := new(bytes.Buffer)
+
// first write the car header to the buffer
hb, err := cbor.DumpObject(&car.CarHeader{
Roots: []cid.Cid{newroot},
Version: 1,
})
if _, err := carstore.LdWrite(buf, hb); err != nil {
return nil, err
}
+
// get a diff of the changes to the repo
+
diffops, err := r.DiffSince(ctx, rootcid)
if err != nil {
return nil, err
}
+
// create the repo ops for the given diff
ops := make([]*atproto.SyncSubscribeRepos_RepoOp, 0, len(diffops))
for _, op := range diffops {
var c cid.Cid
switch op.Op {
···
})
}
+
blk, err := dbs.Get(ctx, c)
if err != nil {
return nil, err
}
+
// write the block to the buffer
if _, err := carstore.LdWrite(buf, blk.Cid().Bytes(), blk.RawData()); err != nil {
return nil, err
}
}
+
// write the writelog to the buffer
for _, op := range bs.GetWriteLog() {
if _, err := carstore.LdWrite(buf, op.Cid().Bytes(), op.RawData()); err != nil {
return nil, err
}
}
+
// blob blob blob blob blob :3
var blobs []lexutil.LexLink
for _, entry := range entries {
var cids []cid.Cid
+
// whenever there is cid present, we know it's a create (dumb)
if entry.Cid != "" {
+
if err := rm.s.db.Create(ctx, &entry, []clause.Expression{clause.OnConflict{
Columns: []clause.Column{{Name: "did"}, {Name: "nsid"}, {Name: "rkey"}},
UpdateAll: true,
}}).Error; err != nil {
return nil, err
}
+
// increment the given blob refs, yay
+
cids, err = rm.incrementBlobRefs(ctx, urepo, entry.Value)
if err != nil {
return nil, err
}
} else {
+
// as i noted above this is dumb. but we delete whenever the cid is nil. it works solely becaue the pkey
+
// is did + collection + rkey. i still really want to separate that out, or use a different type to make
+
// this less confusing/easy to read. alas, its 2 am and yea no
+
if err := rm.s.db.Delete(ctx, &entry, nil).Error; err != nil {
return nil, err
}
+
+
// TODO:
+
cids, err = rm.decrementBlobRefs(ctx, urepo, entry.Value)
if err != nil {
return nil, err
}
}
+
// add all the relevant blobs to the blobs list of blobs. blob ^.^
for _, c := range cids {
blobs = append(blobs, lexutil.LexLink(c))
}
}
+
// NOTE: using the request ctx seems a bit suss here, so using a background context. i'm not sure if this
+
// runs sync or not
+
rm.s.evtman.AddEvent(context.Background(), &events.XRPCStreamEvent{
RepoCommit: &atproto.SyncSubscribeRepos_Commit{
Repo: urepo.Did,
Blocks: buf.Bytes(),
···
},
})
+
if err := rm.s.UpdateRepo(ctx, urepo.Did, newroot, rev); err != nil {
return nil, err
}
···
return results, nil
}
+
// this is a fun little guy. to get a proof, we need to read the record out of the blockstore and record how we actually
+
// got to the guy. we'll wrap a new blockstore in a recording blockstore, then return the log for proof
+
func (rm *RepoMan) getRecordProof(ctx context.Context, urepo models.Repo, collection, rkey string) (cid.Cid, []blocks.Block, error) {
c, err := cid.Cast(urepo.Root)
if err != nil {
return cid.Undef, nil, err
···
dbs := rm.s.getBlockstore(urepo.Did)
bs := recording_blockstore.New(dbs)
+
r, err := repo.OpenRepo(ctx, bs, c)
if err != nil {
return cid.Undef, nil, err
}
+
_, _, err = r.GetRecordBytes(ctx, fmt.Sprintf("%s/%s", collection, rkey))
if err != nil {
return cid.Undef, nil, err
}
···
return c, bs.GetReadLog(), nil
}
+
func (rm *RepoMan) incrementBlobRefs(ctx context.Context, urepo models.Repo, cbor []byte) ([]cid.Cid, error) {
cids, err := getBlobCidsFromCbor(cbor)
if err != nil {
return nil, err
}
for _, c := range cids {
+
if err := rm.db.Exec(ctx, "UPDATE blobs SET ref_count = ref_count + 1 WHERE did = ? AND cid = ?", nil, urepo.Did, c.Bytes()).Error; err != nil {
return nil, err
}
}
···
return cids, nil
}
+
func (rm *RepoMan) decrementBlobRefs(ctx context.Context, urepo models.Repo, cbor []byte) ([]cid.Cid, error) {
cids, err := getBlobCidsFromCbor(cbor)
if err != nil {
return nil, err
···
ID uint
Count int
}
+
if err := rm.db.Raw(ctx, "UPDATE blobs SET ref_count = ref_count - 1 WHERE did = ? AND cid = ? RETURNING id, ref_count", nil, urepo.Did, c.Bytes()).Scan(&res).Error; err != nil {
return nil, err
}
+
// TODO: this does _not_ handle deletions of blobs that are on s3 storage!!!! we need to get the blob, see what
+
// storage it is in, and clean up s3!!!!
if res.Count == 0 {
+
if err := rm.db.Exec(ctx, "DELETE FROM blobs WHERE id = ?", nil, res.ID).Error; err != nil {
return nil, err
}
+
if err := rm.db.Exec(ctx, "DELETE FROM blob_parts WHERE blob_id = ?", nil, res.ID).Error; err != nil {
return nil, err
}
}
+39 -28
server/server.go
···
"github.com/haileyok/cocoon/oauth/provider"
"github.com/haileyok/cocoon/plc"
"github.com/ipfs/go-cid"
echo_session "github.com/labstack/echo-contrib/session"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
···
}
type Args struct {
Addr string
DbName string
DbType string
DatabaseURL string
-
Logger *slog.Logger
Version string
Did string
Hostname string
···
}
func New(args *Args) (*Server, error) {
if args.Addr == "" {
return nil, fmt.Errorf("addr must be set")
}
···
return nil, fmt.Errorf("admin password must be set")
}
-
if args.Logger == nil {
-
args.Logger = slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{}))
-
}
-
if args.SessionSecret == "" {
panic("SESSION SECRET WAS NOT SET. THIS IS REQUIRED. ")
}
···
e := echo.New()
e.Pre(middleware.RemoveTrailingSlash())
-
e.Pre(slogecho.New(args.Logger))
e.Use(echo_session.Middleware(sessions.NewCookieStore([]byte(args.SessionSecret))))
e.Use(middleware.CORSWithConfig(middleware.CORSConfig{
AllowOrigins: []string{"*"},
AllowHeaders: []string{"*"},
···
if err != nil {
return nil, fmt.Errorf("failed to connect to postgres: %w", err)
}
-
args.Logger.Info("connected to PostgreSQL database")
default:
gdb, err = gorm.Open(sqlite.Open(args.DbName), &gorm.Config{})
if err != nil {
return nil, fmt.Errorf("failed to open sqlite database: %w", err)
}
-
args.Logger.Info("connected to SQLite database", "path", args.DbName)
}
dbw := db.NewDB(gdb)
···
var nonceSecret []byte
maybeSecret, err := os.ReadFile("nonce.secret")
if err != nil && !os.IsNotExist(err) {
-
args.Logger.Error("error attempting to read nonce secret", "error", err)
} else {
nonceSecret = maybeSecret
}
···
Hostname: args.Hostname,
ClientManagerArgs: client.ManagerArgs{
Cli: oauthCli,
-
Logger: args.Logger,
},
DpopManagerArgs: dpop.ManagerArgs{
NonceSecret: nonceSecret,
NonceRotationInterval: constants.NonceMaxRotationInterval / 3,
OnNonceSecretCreated: func(newNonce []byte) {
if err := os.WriteFile("nonce.secret", newNonce, 0644); err != nil {
-
args.Logger.Error("error writing new nonce secret", "error", err)
}
},
-
Logger: args.Logger,
Hostname: args.Hostname,
},
}),
···
}
func (s *Server) Serve(ctx context.Context) error {
s.addRoutes()
-
s.logger.Info("migrating...")
s.db.AutoMigrate(
&models.Actor{},
···
&provider.OauthAuthorizationRequest{},
)
-
s.logger.Info("starting cocoon")
go func() {
if err := s.httpd.ListenAndServe(); err != nil {
···
go func() {
if err := s.requestCrawl(ctx); err != nil {
-
s.logger.Error("error requesting crawls", "err", err)
}
}()
···
logger.Info("requesting crawl with configured relays")
-
if time.Now().Sub(s.lastRequestCrawl) <= 1*time.Minute {
return fmt.Errorf("a crawl request has already been made within the last minute")
}
···
}
func (s *Server) doBackup() {
if s.dbType == "postgres" {
-
s.logger.Info("skipping S3 backup - PostgreSQL backups should be handled externally (pg_dump, managed database backups, etc.)")
return
}
start := time.Now()
-
s.logger.Info("beginning backup to s3...")
var buf bytes.Buffer
if err := func() error {
-
s.logger.Info("reading database bytes...")
s.db.Lock()
defer s.db.Unlock()
···
return nil
}(); err != nil {
-
s.logger.Error("error backing up database", "error", err)
return
}
if err := func() error {
-
s.logger.Info("sending to s3...")
currTime := time.Now().Format("2006-01-02_15-04-05")
key := "cocoon-backup-" + currTime + ".db"
···
return fmt.Errorf("error uploading file to s3: %w", err)
}
-
s.logger.Info("finished uploading backup to s3", "key", key, "duration", time.Now().Sub(start).Seconds())
return nil
}(); err != nil {
-
s.logger.Error("error uploading database backup", "error", err)
return
}
···
}
func (s *Server) backupRoutine() {
if s.s3Config == nil || !s.s3Config.BackupsEnabled {
return
}
if s.s3Config.Region == "" {
-
s.logger.Warn("no s3 region configured but backups are enabled. backups will not run.")
return
}
if s.s3Config.Bucket == "" {
-
s.logger.Warn("no s3 bucket configured but backups are enabled. backups will not run.")
return
}
if s.s3Config.AccessKey == "" {
-
s.logger.Warn("no s3 access key configured but backups are enabled. backups will not run.")
return
}
if s.s3Config.SecretKey == "" {
-
s.logger.Warn("no s3 secret key configured but backups are enabled. backups will not run.")
return
}
···
}
func (s *Server) UpdateRepo(ctx context.Context, did string, root cid.Cid, rev string) error {
-
if err := s.db.Exec("UPDATE repos SET root = ?, rev = ? WHERE did = ?", nil, root.Bytes(), rev, did).Error; err != nil {
return err
}
···
"github.com/haileyok/cocoon/oauth/provider"
"github.com/haileyok/cocoon/plc"
"github.com/ipfs/go-cid"
+
"github.com/labstack/echo-contrib/echoprometheus"
echo_session "github.com/labstack/echo-contrib/session"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
···
}
type Args struct {
+
Logger *slog.Logger
+
Addr string
DbName string
DbType string
DatabaseURL string
Version string
Did string
Hostname string
···
}
func New(args *Args) (*Server, error) {
+
if args.Logger == nil {
+
args.Logger = slog.Default()
+
}
+
+
logger := args.Logger.With("name", "New")
+
if args.Addr == "" {
return nil, fmt.Errorf("addr must be set")
}
···
return nil, fmt.Errorf("admin password must be set")
}
if args.SessionSecret == "" {
panic("SESSION SECRET WAS NOT SET. THIS IS REQUIRED. ")
}
···
e := echo.New()
e.Pre(middleware.RemoveTrailingSlash())
+
e.Pre(slogecho.New(args.Logger.With("component", "slogecho")))
e.Use(echo_session.Middleware(sessions.NewCookieStore([]byte(args.SessionSecret))))
+
e.Use(echoprometheus.NewMiddleware("cocoon"))
e.Use(middleware.CORSWithConfig(middleware.CORSConfig{
AllowOrigins: []string{"*"},
AllowHeaders: []string{"*"},
···
if err != nil {
return nil, fmt.Errorf("failed to connect to postgres: %w", err)
}
+
logger.Info("connected to PostgreSQL database")
default:
gdb, err = gorm.Open(sqlite.Open(args.DbName), &gorm.Config{})
if err != nil {
return nil, fmt.Errorf("failed to open sqlite database: %w", err)
}
+
logger.Info("connected to SQLite database", "path", args.DbName)
}
dbw := db.NewDB(gdb)
···
var nonceSecret []byte
maybeSecret, err := os.ReadFile("nonce.secret")
if err != nil && !os.IsNotExist(err) {
+
logger.Error("error attempting to read nonce secret", "error", err)
} else {
nonceSecret = maybeSecret
}
···
Hostname: args.Hostname,
ClientManagerArgs: client.ManagerArgs{
Cli: oauthCli,
+
Logger: args.Logger.With("component", "oauth-client-manager"),
},
DpopManagerArgs: dpop.ManagerArgs{
NonceSecret: nonceSecret,
NonceRotationInterval: constants.NonceMaxRotationInterval / 3,
OnNonceSecretCreated: func(newNonce []byte) {
if err := os.WriteFile("nonce.secret", newNonce, 0644); err != nil {
+
logger.Error("error writing new nonce secret", "error", err)
}
},
+
Logger: args.Logger.With("component", "dpop-manager"),
Hostname: args.Hostname,
},
}),
···
}
func (s *Server) Serve(ctx context.Context) error {
+
logger := s.logger.With("name", "Serve")
+
s.addRoutes()
+
logger.Info("migrating...")
s.db.AutoMigrate(
&models.Actor{},
···
&provider.OauthAuthorizationRequest{},
)
+
logger.Info("starting cocoon")
go func() {
if err := s.httpd.ListenAndServe(); err != nil {
···
go func() {
if err := s.requestCrawl(ctx); err != nil {
+
logger.Error("error requesting crawls", "err", err)
}
}()
···
logger.Info("requesting crawl with configured relays")
+
if time.Since(s.lastRequestCrawl) <= 1*time.Minute {
return fmt.Errorf("a crawl request has already been made within the last minute")
}
···
}
func (s *Server) doBackup() {
+
logger := s.logger.With("name", "doBackup")
+
if s.dbType == "postgres" {
+
logger.Info("skipping S3 backup - PostgreSQL backups should be handled externally (pg_dump, managed database backups, etc.)")
return
}
start := time.Now()
+
logger.Info("beginning backup to s3...")
var buf bytes.Buffer
if err := func() error {
+
logger.Info("reading database bytes...")
s.db.Lock()
defer s.db.Unlock()
···
return nil
}(); err != nil {
+
logger.Error("error backing up database", "error", err)
return
}
if err := func() error {
+
logger.Info("sending to s3...")
currTime := time.Now().Format("2006-01-02_15-04-05")
key := "cocoon-backup-" + currTime + ".db"
···
return fmt.Errorf("error uploading file to s3: %w", err)
}
+
logger.Info("finished uploading backup to s3", "key", key, "duration", time.Now().Sub(start).Seconds())
return nil
}(); err != nil {
+
logger.Error("error uploading database backup", "error", err)
return
}
···
}
func (s *Server) backupRoutine() {
+
logger := s.logger.With("name", "backupRoutine")
+
if s.s3Config == nil || !s.s3Config.BackupsEnabled {
return
}
if s.s3Config.Region == "" {
+
logger.Warn("no s3 region configured but backups are enabled. backups will not run.")
return
}
if s.s3Config.Bucket == "" {
+
logger.Warn("no s3 bucket configured but backups are enabled. backups will not run.")
return
}
if s.s3Config.AccessKey == "" {
+
logger.Warn("no s3 access key configured but backups are enabled. backups will not run.")
return
}
if s.s3Config.SecretKey == "" {
+
logger.Warn("no s3 secret key configured but backups are enabled. backups will not run.")
return
}
···
}
func (s *Server) UpdateRepo(ctx context.Context, did string, root cid.Cid, rev string) error {
+
if err := s.db.Exec(ctx, "UPDATE repos SET root = ?, rev = ? WHERE did = ?", nil, root.Bytes(), rev, did).Error; err != nil {
return err
}
+4 -3
server/session.go
···
package server
import (
"time"
"github.com/golang-jwt/jwt/v4"
···
RefreshToken string
}
-
func (s *Server) createSession(repo *models.Repo) (*Session, error) {
now := time.Now()
accexp := now.Add(3 * time.Hour)
refexp := now.Add(7 * 24 * time.Hour)
···
return nil, err
}
-
if err := s.db.Create(&models.Token{
Token: accessString,
Did: repo.Did,
RefreshToken: refreshString,
···
return nil, err
}
-
if err := s.db.Create(&models.RefreshToken{
Token: refreshString,
Did: repo.Did,
CreatedAt: now,
···
package server
import (
+
"context"
"time"
"github.com/golang-jwt/jwt/v4"
···
RefreshToken string
}
+
func (s *Server) createSession(ctx context.Context, repo *models.Repo) (*Session, error) {
now := time.Now()
accexp := now.Add(3 * time.Hour)
refexp := now.Add(7 * 24 * time.Hour)
···
return nil, err
}
+
if err := s.db.Create(ctx, &models.Token{
Token: accessString,
Did: repo.Did,
RefreshToken: refreshString,
···
return nil, err
}
+
if err := s.db.Create(ctx, &models.RefreshToken{
Token: refreshString,
Did: repo.Did,
CreatedAt: now,
+4
server/templates/signin.html
···
type="password"
placeholder="Password"
/>
<input name="query_params" type="hidden" value="{{ .QueryParams }}" />
<button class="primary" type="submit" value="Login">Login</button>
</form>
···
type="password"
placeholder="Password"
/>
+
{{ if .flashes.tokenrequired }}
+
<br />
+
<input name="token" id="token" placeholder="Enter your 2FA token" />
+
{{ end }}
<input name="query_params" type="hidden" value="{{ .QueryParams }}" />
<button class="primary" type="submit" value="Login">Login</button>
</form>
+3 -3
sqlite_blockstore/sqlite_blockstore.go
···
return maybeBlock, nil
}
-
if err := bs.db.Raw("SELECT * FROM blocks WHERE did = ? AND cid = ?", nil, bs.did, cid.Bytes()).Scan(&block).Error; err != nil {
return nil, err
}
···
Value: block.RawData(),
}
-
if err := bs.db.Create(&b, []clause.Expression{clause.OnConflict{
Columns: []clause.Column{{Name: "did"}, {Name: "cid"}},
UpdateAll: true,
}}).Error; err != nil {
···
}
func (bs *SqliteBlockstore) PutMany(ctx context.Context, blocks []blocks.Block) error {
-
tx := bs.db.BeginDangerously()
for _, block := range blocks {
bs.inserts[block.Cid()] = block
···
return maybeBlock, nil
}
+
if err := bs.db.Raw(ctx, "SELECT * FROM blocks WHERE did = ? AND cid = ?", nil, bs.did, cid.Bytes()).Scan(&block).Error; err != nil {
return nil, err
}
···
Value: block.RawData(),
}
+
if err := bs.db.Create(ctx, &b, []clause.Expression{clause.OnConflict{
Columns: []clause.Column{{Name: "did"}, {Name: "cid"}},
UpdateAll: true,
}}).Error; err != nil {
···
}
func (bs *SqliteBlockstore) PutMany(ctx context.Context, blocks []blocks.Block) error {
+
tx := bs.db.BeginDangerously(ctx)
for _, block := range blocks {
bs.inserts[block.Cid()] = block
+1 -1
test.go
···
u.Path = "xrpc/com.atproto.sync.subscribeRepos"
conn, _, err := dialer.Dial(u.String(), http.Header{
-
"User-Agent": []string{fmt.Sprintf("hot-topic/0.0.0")},
})
if err != nil {
return fmt.Errorf("subscribing to firehose failed (dialing): %w", err)
···
u.Path = "xrpc/com.atproto.sync.subscribeRepos"
conn, _, err := dialer.Dial(u.String(), http.Header{
+
"User-Agent": []string{"cocoon-test/0.0.0"},
})
if err != nil {
return fmt.Errorf("subscribing to firehose failed (dialing): %w", err)