Welcome, {{ .Repo.Handle }}
--
-
- Sign Out -
{{ index .flashes.successes 0 }}
-{{ .ClientName }}
-Session Age: {{ .Age}}
-Last Updated: {{ .LastUpdated }} ago
-Expires In: {{ .ExpiresIn }}
-IP Address: {{ .Ip }}
- -diff --git a/cmd/cocoon/main.go b/cmd/cocoon/main.go deleted file mode 100644 index e10606f..0000000 --- a/cmd/cocoon/main.go +++ /dev/null @@ -1,432 +0,0 @@ -package main - -import ( - "crypto/ecdsa" - "crypto/elliptic" - "crypto/rand" - "encoding/json" - "fmt" - "log/slog" - "os" - "strings" - "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" - "github.com/haileyok/cocoon/server" - _ "github.com/joho/godotenv/autoload" - "github.com/lestrrat-go/jwx/v2/jwk" - "github.com/urfave/cli/v2" - "golang.org/x/crypto/bcrypt" - "gorm.io/driver/postgres" - "gorm.io/driver/sqlite" - "gorm.io/gorm" -) - -var Version = "dev" - -func main() { - app := &cli.App{ - Name: "cocoon", - Usage: "An atproto PDS", - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "addr", - Value: ":8080", - EnvVars: []string{"COCOON_ADDR"}, - }, - &cli.StringFlag{ - Name: "db-name", - Value: "cocoon.db", - EnvVars: []string{"COCOON_DB_NAME"}, - }, - &cli.StringFlag{ - Name: "db-type", - Value: "sqlite", - Usage: "Database type: sqlite or postgres", - EnvVars: []string{"COCOON_DB_TYPE"}, - }, - &cli.StringFlag{ - Name: "database-url", - Aliases: []string{"db-url"}, - Usage: "PostgreSQL connection string (required if db-type is postgres)", - EnvVars: []string{"COCOON_DATABASE_URL", "DATABASE_URL"}, - }, - &cli.StringFlag{ - Name: "did", - EnvVars: []string{"COCOON_DID"}, - }, - &cli.StringFlag{ - Name: "hostname", - EnvVars: []string{"COCOON_HOSTNAME"}, - }, - &cli.StringFlag{ - Name: "rotation-key-path", - EnvVars: []string{"COCOON_ROTATION_KEY_PATH"}, - }, - &cli.StringFlag{ - Name: "jwk-path", - EnvVars: []string{"COCOON_JWK_PATH"}, - }, - &cli.StringFlag{ - Name: "contact-email", - EnvVars: []string{"COCOON_CONTACT_EMAIL"}, - }, - &cli.StringSliceFlag{ - Name: "relays", - EnvVars: []string{"COCOON_RELAYS"}, - }, - &cli.StringFlag{ - Name: "admin-password", - EnvVars: []string{"COCOON_ADMIN_PASSWORD"}, - }, - &cli.BoolFlag{ - Name: "require-invite", - EnvVars: []string{"COCOON_REQUIRE_INVITE"}, - Value: true, - }, - &cli.StringFlag{ - Name: "smtp-user", - EnvVars: []string{"COCOON_SMTP_USER"}, - }, - &cli.StringFlag{ - Name: "smtp-pass", - EnvVars: []string{"COCOON_SMTP_PASS"}, - }, - &cli.StringFlag{ - Name: "smtp-host", - EnvVars: []string{"COCOON_SMTP_HOST"}, - }, - &cli.StringFlag{ - Name: "smtp-port", - EnvVars: []string{"COCOON_SMTP_PORT"}, - }, - &cli.StringFlag{ - Name: "smtp-email", - EnvVars: []string{"COCOON_SMTP_EMAIL"}, - }, - &cli.StringFlag{ - Name: "smtp-name", - EnvVars: []string{"COCOON_SMTP_NAME"}, - }, - &cli.BoolFlag{ - Name: "s3-backups-enabled", - EnvVars: []string{"COCOON_S3_BACKUPS_ENABLED"}, - }, - &cli.BoolFlag{ - Name: "s3-blobstore-enabled", - EnvVars: []string{"COCOON_S3_BLOBSTORE_ENABLED"}, - }, - &cli.StringFlag{ - Name: "s3-region", - EnvVars: []string{"COCOON_S3_REGION"}, - }, - &cli.StringFlag{ - Name: "s3-bucket", - EnvVars: []string{"COCOON_S3_BUCKET"}, - }, - &cli.StringFlag{ - Name: "s3-endpoint", - EnvVars: []string{"COCOON_S3_ENDPOINT"}, - }, - &cli.StringFlag{ - Name: "s3-access-key", - EnvVars: []string{"COCOON_S3_ACCESS_KEY"}, - }, - &cli.StringFlag{ - Name: "s3-secret-key", - EnvVars: []string{"COCOON_S3_SECRET_KEY"}, - }, - &cli.StringFlag{ - Name: "s3-cdn-url", - EnvVars: []string{"COCOON_S3_CDN_URL"}, - Usage: "Public URL for S3 blob redirects (e.g., https://cdn.example.com). When set, getBlob redirects to this URL instead of proxying.", - }, - &cli.StringFlag{ - Name: "session-secret", - EnvVars: []string{"COCOON_SESSION_SECRET"}, - }, - &cli.StringFlag{ - Name: "session-cookie-key", - EnvVars: []string{"COCOON_SESSION_COOKIE_KEY"}, - Value: "session", - }, - &cli.StringFlag{ - Name: "blockstore-variant", - EnvVars: []string{"COCOON_BLOCKSTORE_VARIANT"}, - Value: "sqlite", - }, - &cli.StringFlag{ - Name: "fallback-proxy", - EnvVars: []string{"COCOON_FALLBACK_PROXY"}, - }, - telemetry.CLIFlagDebug, - telemetry.CLIFlagMetricsListenAddress, - }, - Commands: []*cli.Command{ - runServe, - runCreateRotationKey, - runCreatePrivateJwk, - runCreateInviteCode, - runResetPassword, - }, - ErrWriter: os.Stdout, - Version: Version, - } - - if err := app.Run(os.Args); err != nil { - fmt.Printf("Error: %v\n", err) - } -} - -var runServe = &cli.Command{ - Name: "run", - Usage: "Start the cocoon PDS", - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "log-level", - Usage: "Log level: debug, info, warn, error", - EnvVars: []string{"COCOON_LOG_LEVEL", "LOG_LEVEL"}, - Value: "info", - }, - }, - Action: func(cmd *cli.Context) error { - - logger := telemetry.StartLogger(cmd) - telemetry.StartMetrics(cmd) - - var level slog.Level - switch strings.ToLower(cmd.String("log-level")) { - case "debug": - level = slog.LevelDebug - case "info": - level = slog.LevelInfo - case "warn": - level = slog.LevelWarn - case "error": - level = slog.LevelError - default: - level = slog.LevelInfo - } - - s, err := server.New(&server.Args{ - Logger: logger, - LogLevel: level, - Addr: cmd.String("addr"), - DbName: cmd.String("db-name"), - DbType: cmd.String("db-type"), - DatabaseURL: cmd.String("database-url"), - Did: cmd.String("did"), - Hostname: cmd.String("hostname"), - RotationKeyPath: cmd.String("rotation-key-path"), - JwkPath: cmd.String("jwk-path"), - ContactEmail: cmd.String("contact-email"), - Version: Version, - Relays: cmd.StringSlice("relays"), - AdminPassword: cmd.String("admin-password"), - RequireInvite: cmd.Bool("require-invite"), - SmtpUser: cmd.String("smtp-user"), - SmtpPass: cmd.String("smtp-pass"), - SmtpHost: cmd.String("smtp-host"), - SmtpPort: cmd.String("smtp-port"), - SmtpEmail: cmd.String("smtp-email"), - SmtpName: cmd.String("smtp-name"), - S3Config: &server.S3Config{ - BackupsEnabled: cmd.Bool("s3-backups-enabled"), - BlobstoreEnabled: cmd.Bool("s3-blobstore-enabled"), - Region: cmd.String("s3-region"), - Bucket: cmd.String("s3-bucket"), - Endpoint: cmd.String("s3-endpoint"), - AccessKey: cmd.String("s3-access-key"), - SecretKey: cmd.String("s3-secret-key"), - CDNUrl: cmd.String("s3-cdn-url"), - }, - SessionSecret: cmd.String("session-secret"), - SessionCookieKey: cmd.String("session-cookie-key"), - BlockstoreVariant: server.MustReturnBlockstoreVariant(cmd.String("blockstore-variant")), - FallbackProxy: cmd.String("fallback-proxy"), - }) - if err != nil { - fmt.Printf("error creating cocoon: %v", err) - return err - } - - if err := s.Serve(cmd.Context); err != nil { - fmt.Printf("error starting cocoon: %v", err) - return err - } - - return nil - }, -} - -var runCreateRotationKey = &cli.Command{ - Name: "create-rotation-key", - Usage: "creates a rotation key for your pds", - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "out", - Required: true, - Usage: "output file for your rotation key", - }, - }, - Action: func(cmd *cli.Context) error { - key, err := atcrypto.GeneratePrivateKeyK256() - if err != nil { - return err - } - - bytes := key.Bytes() - - if err := os.WriteFile(cmd.String("out"), bytes, 0644); err != nil { - return err - } - - return nil - }, -} - -var runCreatePrivateJwk = &cli.Command{ - Name: "create-private-jwk", - Usage: "creates a private jwk for your pds", - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "out", - Required: true, - Usage: "output file for your jwk", - }, - }, - Action: func(cmd *cli.Context) error { - privKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - if err != nil { - return err - } - - key, err := jwk.FromRaw(privKey) - if err != nil { - return err - } - - kid := fmt.Sprintf("%d", time.Now().Unix()) - - if err := key.Set(jwk.KeyIDKey, kid); err != nil { - return err - } - - b, err := json.Marshal(key) - if err != nil { - return err - } - - if err := os.WriteFile(cmd.String("out"), b, 0644); err != nil { - return err - } - - return nil - }, -} - -var runCreateInviteCode = &cli.Command{ - Name: "create-invite-code", - Usage: "creates an invite code", - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "for", - Usage: "optional did to assign the invite code to", - }, - &cli.IntFlag{ - Name: "uses", - Usage: "number of times the invite code can be used", - Value: 1, - }, - }, - Action: func(cmd *cli.Context) error { - db, err := newDb(cmd) - if err != nil { - return err - } - - forDid := "did:plc:123" - if cmd.String("for") != "" { - did, err := syntax.ParseDID(cmd.String("for")) - if err != nil { - return err - } - - forDid = did.String() - } - - uses := cmd.Int("uses") - - code := fmt.Sprintf("%s-%s", helpers.RandomVarchar(8), helpers.RandomVarchar(8)) - - if err := db.Exec("INSERT INTO invite_codes (did, code, remaining_use_count) VALUES (?, ?, ?)", forDid, code, uses).Error; err != nil { - return err - } - - fmt.Printf("New invite code created with %d uses: %s\n", uses, code) - - return nil - }, -} - -var runResetPassword = &cli.Command{ - Name: "reset-password", - Usage: "resets a password", - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "did", - Usage: "did of the user who's password you want to reset", - }, - }, - Action: func(cmd *cli.Context) error { - db, err := newDb(cmd) - if err != nil { - return err - } - - didStr := cmd.String("did") - did, err := syntax.ParseDID(didStr) - if err != nil { - return err - } - - newPass := fmt.Sprintf("%s-%s", helpers.RandomVarchar(12), helpers.RandomVarchar(12)) - hashed, err := bcrypt.GenerateFromPassword([]byte(newPass), 10) - if err != nil { - return err - } - - if err := db.Exec("UPDATE repos SET password = ? WHERE did = ?", hashed, did.String()).Error; err != nil { - return err - } - - fmt.Printf("Password for %s has been reset to: %s", did.String(), newPass) - - return nil - }, -} - -func newDb(cmd *cli.Context) (*gorm.DB, error) { - dbType := cmd.String("db-type") - if dbType == "" { - dbType = "sqlite" - } - - switch dbType { - case "postgres": - databaseURL := cmd.String("database-url") - if databaseURL == "" { - return nil, fmt.Errorf("COCOON_DATABASE_URL or DATABASE_URL must be set when using postgres") - } - return gorm.Open(postgres.Open(databaseURL), &gorm.Config{}) - default: - dbName := cmd.String("db-name") - if dbName == "" { - dbName = "cocoon.db" - } - return gorm.Open(sqlite.Open(dbName), &gorm.Config{}) - } -} diff --git a/go.mod b/go.mod index a31bcdb..a5b8719 100644 --- a/go.mod +++ b/go.mod @@ -1,38 +1,27 @@ -module github.com/haileyok/cocoon +module tangled.org/pdewey.com/chrysalis go 1.26.1 require ( - github.com/aws/aws-sdk-go v1.55.8 - github.com/bluesky-social/go-util v0.0.0-20260221095237-38d8022e3dc1 github.com/bluesky-social/indigo v0.0.0-20260308004230-c55a189a51a9 - 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.1.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.3 - github.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b github.com/hashicorp/golang-lru/v2 v2.0.7 github.com/ipfs/go-block-format v0.2.3 github.com/ipfs/go-cid v0.6.0 github.com/ipfs/go-ipfs-blockstore v1.3.1 github.com/ipfs/go-ipld-cbor v0.2.1 github.com/ipld/go-car v0.6.3 - github.com/joho/godotenv v1.5.1 - github.com/labstack/echo-contrib v0.50.1 github.com/labstack/echo/v4 v4.15.1 github.com/lestrrat-go/jwx/v2 v2.1.6 github.com/multiformats/go-multihash v0.2.3 - github.com/prometheus/client_golang v1.23.2 github.com/samber/slog-echo v1.21.0 - github.com/urfave/cli/v2 v2.27.7 github.com/whyrusleeping/cbor-gen v0.3.1 gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b golang.org/x/crypto v0.48.0 - gorm.io/driver/postgres v1.6.0 gorm.io/driver/sqlite v1.6.0 gorm.io/gorm v1.31.1 ) @@ -41,7 +30,6 @@ require ( github.com/RussellLuo/slidingwindow v0.0.0-20200528002341-535bb99d338b // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1 // indirect github.com/earthboundkid/versioninfo/v2 v2.24.1 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect @@ -55,8 +43,6 @@ require ( github.com/gocql/gocql v1.7.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/snappy v1.0.0 // indirect - github.com/gorilla/context v1.1.2 // indirect - 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.8 // indirect @@ -82,7 +68,6 @@ require ( github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect - github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/labstack/gommon v0.4.2 // indirect github.com/leodido/go-urn v1.4.0 // indirect @@ -104,16 +89,15 @@ require ( 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.20231129105047-37766d95467a // indirect + github.com/prometheus/client_golang v1.23.2 // indirect github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/common v0.67.5 // indirect github.com/prometheus/procfs v0.20.1 // indirect - github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/samber/lo v1.53.0 // indirect github.com/segmentio/asm v1.2.1 // indirect github.com/spaolacci/murmur3 v1.1.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasttemplate v1.2.2 // indirect - github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342 // indirect gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 // indirect @@ -133,5 +117,6 @@ require ( google.golang.org/protobuf v1.36.11 // indirect gopkg.in/go-playground/assert.v1 v1.2.1 // indirect gopkg.in/inf.v0 v0.9.1 // indirect + gorm.io/driver/postgres v1.6.0 // indirect lukechampine.com/blake3 v1.4.1 // indirect ) diff --git a/go.sum b/go.sum index 25cd0b9..10bdeb2 100644 --- a/go.sum +++ b/go.sum @@ -47,8 +47,6 @@ github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kd github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= -github.com/aws/aws-sdk-go v1.55.8 h1:JRmEUbU52aJQZ2AjX4q4Wu7t4uZjOu71uyNmaWlUkJQ= -github.com/aws/aws-sdk-go v1.55.8/go.mod h1:ZkViS9AqA6otK+JBBNH2++sx1sgxrPKcSzPPvQkUtXk= github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o= github.com/benbjohnson/clock v1.3.5/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= @@ -57,14 +55,10 @@ github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kB 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/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM= -github.com/bluesky-social/go-util v0.0.0-20260221095237-38d8022e3dc1 h1:I+rZWV1r077OFGcbqA9i0wHQnb54j2ZCXSp9oFNOzVE= -github.com/bluesky-social/go-util v0.0.0-20260221095237-38d8022e3dc1/go.mod h1:LWamyZfbQGW7PaVc5jumFfjgrshJ5mXgDUnR6fK7+BI= github.com/bluesky-social/indigo v0.0.0-20260308004230-c55a189a51a9 h1:nzW5kShDFLv2CRWGUg5HDWfh8ctnyAAeasXKVTKZy8I= github.com/bluesky-social/indigo v0.0.0-20260308004230-c55a189a51a9/go.mod h1:VG/LeqLGNI3Ew7lsYixajnZGFfWPv144qbUddh+Oyag= github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY= github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= -github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792 h1:R8vQdOQdZ9Y3SkEwmHoWBmX1DNXhXZqlTpq6s4tyJGc= -github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= @@ -79,8 +73,6 @@ github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3Ee github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= -github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo= -github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/cskr/pubsub v1.0.2 h1:vlOzMhl6PFn60gRlTQQsIfVwaPB/B/8MziK8FhEPt/0= github.com/cskr/pubsub v1.0.2/go.mod h1:/8MzYXk/NJAz782G8RPkFzXTZVu63VotefPnR9TIRis= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -88,8 +80,6 @@ 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.4.1 h1:5RVFMOWjMyRy8cARdy79nAmgYw3hK/4HUq48LQ6Wwqo= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= -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/earthboundkid/versioninfo/v2 v2.24.1 h1:SJTMHaoUx3GzjjnUO1QzP3ZXK6Ee/nbWyCm58eY3oUg= github.com/earthboundkid/versioninfo/v2 v2.24.1/go.mod h1:VcWEooDEuyUJnMfbdTh0uFN4cfEIg+kHMuWB2CDCLjw= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= @@ -191,8 +181,6 @@ github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= -github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= @@ -216,19 +204,11 @@ github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5m github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gopherjs/gopherjs v1.17.2 h1:fQnZVsXk8uxXIStYb0N4bGk7jeyTalG/wsZjQ25dO0g= github.com/gopherjs/gopherjs v1.17.2/go.mod h1:pRRIvn/QzFLrKfvEz3qUuEhtE/zLCWfreZ6J5gM2i+k= -github.com/gorilla/context v1.1.2 h1:WRkNAv2uoa03QNIc1A6u4O7DAGMUVoopZhkiXWA2V1o= -github.com/gorilla/context v1.1.2/go.mod h1:KDPwT9i/MeWHiLl90fuTgrt4/wPcv75vFAZLaOOcbxM= -github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= -github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= -github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ= -github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed h1:5upAirOpQc1Q53c0bnx2ufif5kANL7bfZWcc6VJWJd8= github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed/go.mod h1:tMWxXQ9wFIaZeTI9F+hmhFiGpFmhOHzyShyFUhRm0H4= -github.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b h1:wDUNC2eKiL35DbLvsDhiblTUXHxcOPwQSCzi7xpQUN4= -github.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b/go.mod h1:VzxiSdG6j1pi7rwGm/xYI5RbtpBgM8sARDXlvEvxlu0= github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -341,12 +321,6 @@ github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= -github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= -github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= -github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= -github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= -github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= -github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= @@ -354,8 +328,6 @@ github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7 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.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/koron/go-ssdp v0.0.6 h1:Jb0h04599eq/CY7rB5YEqPS83HmRfHP2azkxMN2rFtU= @@ -368,10 +340,6 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/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.50.1 h1:W9cZZ9viA4TDdFtm8cuA+XGFwOcnfbjJpl7VgfsRLHE= -github.com/labstack/echo-contrib v0.50.1/go.mod h1:8r/++U/Fw/QniApFnzunLanKaviPfBX7fX7/2QX0qOk= github.com/labstack/echo/v4 v4.15.1 h1:S9keusg26gZpjMmPqB5hOEvNKnmd1lNmcHrbbH2lnFs= github.com/labstack/echo/v4 v4.15.1/go.mod h1:xmw1clThob0BSVRX1CRQkGQ/vjwcpOMjQZSZa9fKA/c= github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= @@ -482,8 +450,6 @@ github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFR github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= -github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/samber/lo v1.53.0 h1:t975lj2py4kJPQ6haz1QMgtId2gtmfktACxIXArw3HM= github.com/samber/lo v1.53.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0= @@ -525,8 +491,6 @@ github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69 github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE= github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ= github.com/urfave/cli v1.22.10/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= -github.com/urfave/cli/v2 v2.27.7 h1:bH59vdhbjLv3LAvIu6gd0usJHgoTTPhCFib8qqOwXYU= -github.com/urfave/cli/v2 v2.27.7/go.mod h1:CyNAG/xg+iAOg0N4MPGZqVmv2rCoP267496AOXUZjA4= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= @@ -539,8 +503,6 @@ github.com/whyrusleeping/cbor v0.0.0-20171005072247-63513f603b11 h1:5HZfQkwe0mIf github.com/whyrusleeping/cbor v0.0.0-20171005072247-63513f603b11/go.mod h1:Wlo/SzPmxVp6vXpGt/zaXhHH0fn4IxgqZc82aKg6bpQ= github.com/whyrusleeping/cbor-gen v0.3.1 h1:82ioxmhEYut7LBVGhGq8xoRkXPLElVuh5mV67AFfdv0= github.com/whyrusleeping/cbor-gen v0.3.1/go.mod h1:pM99HXyEbSQHcosHc0iW7YFmwnscr+t9Te4ibko05so= -github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342 h1:FnBeRrxr7OU4VvAzt5X7s6266i6cSVkkFPS0TuXWbIg= -github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= diff --git a/metrics/metrics.go b/metrics/metrics.go deleted file mode 100644 index aa8298c..0000000 --- a/metrics/metrics.go +++ /dev/null @@ -1,30 +0,0 @@ -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"}) -) diff --git a/oauth/client/manager.go b/oauth/client/manager.go index 6b0f077..a4d2f72 100644 --- a/oauth/client/manager.go +++ b/oauth/client/manager.go @@ -14,7 +14,7 @@ import ( "time" cache "github.com/go-pkgz/expirable-cache/v3" - "github.com/haileyok/cocoon/internal/helpers" + "tangled.org/pdewey.com/chrysalis/internal/helpers" "github.com/lestrrat-go/jwx/v2/jwk" ) diff --git a/oauth/dpop/jti_cache.go b/oauth/dpop/jti_cache.go index cc6c7af..8121847 100644 --- a/oauth/dpop/jti_cache.go +++ b/oauth/dpop/jti_cache.go @@ -5,7 +5,7 @@ import ( "time" cache "github.com/go-pkgz/expirable-cache/v3" - "github.com/haileyok/cocoon/oauth/constants" + "tangled.org/pdewey.com/chrysalis/oauth/constants" ) type jtiCache struct { diff --git a/oauth/dpop/manager.go b/oauth/dpop/manager.go index 103e004..6237112 100644 --- a/oauth/dpop/manager.go +++ b/oauth/dpop/manager.go @@ -14,8 +14,8 @@ import ( "time" "github.com/golang-jwt/jwt/v4" - "github.com/haileyok/cocoon/internal/helpers" - "github.com/haileyok/cocoon/oauth/constants" + "tangled.org/pdewey.com/chrysalis/internal/helpers" + "tangled.org/pdewey.com/chrysalis/oauth/constants" "github.com/lestrrat-go/jwx/v2/jwa" "github.com/lestrrat-go/jwx/v2/jwk" ) diff --git a/oauth/dpop/nonce.go b/oauth/dpop/nonce.go index 8bfe003..29605c3 100644 --- a/oauth/dpop/nonce.go +++ b/oauth/dpop/nonce.go @@ -8,8 +8,8 @@ import ( "sync" "time" - "github.com/haileyok/cocoon/internal/helpers" - "github.com/haileyok/cocoon/oauth/constants" + "tangled.org/pdewey.com/chrysalis/internal/helpers" + "tangled.org/pdewey.com/chrysalis/oauth/constants" ) type Nonce struct { diff --git a/oauth/helpers.go b/oauth/helpers.go index 5a183c3..ea18f8d 100644 --- a/oauth/helpers.go +++ b/oauth/helpers.go @@ -6,9 +6,9 @@ import ( "net/url" "time" - "github.com/haileyok/cocoon/internal/helpers" - "github.com/haileyok/cocoon/oauth/constants" - "github.com/haileyok/cocoon/oauth/provider" + "tangled.org/pdewey.com/chrysalis/internal/helpers" + "tangled.org/pdewey.com/chrysalis/oauth/constants" + "tangled.org/pdewey.com/chrysalis/oauth/provider" ) func GenerateCode() string { diff --git a/oauth/provider/client_auth.go b/oauth/provider/client_auth.go index 1e4a620..e224df3 100644 --- a/oauth/provider/client_auth.go +++ b/oauth/provider/client_auth.go @@ -9,9 +9,9 @@ import ( "time" "github.com/golang-jwt/jwt/v4" - "github.com/haileyok/cocoon/oauth/client" - "github.com/haileyok/cocoon/oauth/constants" - "github.com/haileyok/cocoon/oauth/dpop" + "tangled.org/pdewey.com/chrysalis/oauth/client" + "tangled.org/pdewey.com/chrysalis/oauth/constants" + "tangled.org/pdewey.com/chrysalis/oauth/dpop" ) type AuthenticateClientOptions struct { diff --git a/oauth/provider/provider.go b/oauth/provider/provider.go index ba22453..8f05f5b 100644 --- a/oauth/provider/provider.go +++ b/oauth/provider/provider.go @@ -1,8 +1,8 @@ package provider import ( - "github.com/haileyok/cocoon/oauth/client" - "github.com/haileyok/cocoon/oauth/dpop" + "tangled.org/pdewey.com/chrysalis/oauth/client" + "tangled.org/pdewey.com/chrysalis/oauth/dpop" ) type Provider struct { diff --git a/plc/client.go b/plc/client.go index e4f6722..da24e51 100644 --- a/plc/client.go +++ b/plc/client.go @@ -15,7 +15,7 @@ import ( "github.com/bluesky-social/indigo/atproto/atcrypto" "github.com/bluesky-social/indigo/util" - "github.com/haileyok/cocoon/identity" + "tangled.org/pdewey.com/chrysalis/identity" ) // PLCClient is the interface used by the server for PLC directory operations. diff --git a/plc/fakeplc.go b/plc/fakeplc.go new file mode 100644 index 0000000..3bfbc89 --- /dev/null +++ b/plc/fakeplc.go @@ -0,0 +1,46 @@ +package plc + +import ( + "context" + "fmt" + "sync" +) + +// FakePLC is a PLCClient that generates valid DIDs locally without contacting +// a real PLC directory. It embeds *Client to reuse the real signing and +// credential logic, and only overrides SendOperation to store operations in +// memory instead of POSTing them. +type FakePLC struct { + *Client + mu sync.Mutex + operations map[string]*Operation +} + +// NewFakePLC creates a FakePLC backed by the given rotation key. +func NewFakePLC(rotationKey []byte, pdsHostname string) (*FakePLC, error) { + c, err := NewClient(&ClientArgs{ + RotationKey: rotationKey, + PdsHostname: pdsHostname, + }) + if err != nil { + return nil, fmt.Errorf("fake plc: %w", err) + } + return &FakePLC{ + Client: c, + operations: make(map[string]*Operation), + }, nil +} + +func (f *FakePLC) SendOperation(_ context.Context, did string, op *Operation) error { + f.mu.Lock() + defer f.mu.Unlock() + f.operations[did] = op + return nil +} + +// GetOperation returns the latest operation stored for a DID, or nil if none. +func (f *FakePLC) GetOperation(did string) *Operation { + f.mu.Lock() + defer f.mu.Unlock() + return f.operations[did] +} diff --git a/plc/types.go b/plc/types.go index 9d905e0..057842c 100644 --- a/plc/types.go +++ b/plc/types.go @@ -4,7 +4,7 @@ import ( "encoding/json" "github.com/bluesky-social/indigo/atproto/atdata" - "github.com/haileyok/cocoon/identity" + "tangled.org/pdewey.com/chrysalis/identity" cbg "github.com/whyrusleeping/cbor-gen" ) diff --git a/server/blockstore_variant.go b/server/blockstore_variant.go index 874f512..435ffe3 100644 --- a/server/blockstore_variant.go +++ b/server/blockstore_variant.go @@ -1,7 +1,7 @@ package server import ( - "github.com/haileyok/cocoon/sqlite_blockstore" + "tangled.org/pdewey.com/chrysalis/sqlite_blockstore" blockstore "github.com/ipfs/go-ipfs-blockstore" ) diff --git a/server/common.go b/server/common.go index e240e03..4f42327 100644 --- a/server/common.go +++ b/server/common.go @@ -3,7 +3,7 @@ package server import ( "context" - "github.com/haileyok/cocoon/models" + "tangled.org/pdewey.com/chrysalis/models" "gorm.io/gorm" ) diff --git a/server/handle_account.go b/server/handle_account.go deleted file mode 100644 index 8ab4450..0000000 --- a/server/handle_account.go +++ /dev/null @@ -1,76 +0,0 @@ -package server - -import ( - "time" - - "github.com/haileyok/cocoon/oauth" - "github.com/haileyok/cocoon/oauth/constants" - "github.com/haileyok/cocoon/oauth/provider" - "github.com/hako/durafmt" - "github.com/labstack/echo/v4" -) - -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{ - "flashes": getFlashesFromSession(e, sess), - }) - } - - var filtered []provider.OauthToken - for _, t := range tokens { - ageRes := oauth.GetSessionAgeFromToken(t) - if ageRes.SessionExpired { - continue - } - filtered = append(filtered, t) - } - - now := time.Now() - - tokenInfo := []map[string]string{} - for _, t := range tokens { - ageRes := oauth.GetSessionAgeFromToken(t) - maxTime := constants.PublicClientSessionLifetime - if t.ClientAuth.Method != "none" { - maxTime = constants.ConfidentialClientSessionLifetime - } - - var clientName string - metadata, err := s.oauthProvider.ClientManager.GetClient(ctx, t.ClientId) - if err != nil { - clientName = t.ClientId - } else { - clientName = metadata.Metadata.ClientName - } - - tokenInfo = append(tokenInfo, map[string]string{ - "ClientName": clientName, - "Age": durafmt.Parse(ageRes.SessionAge).LimitFirstN(2).String(), - "LastUpdated": durafmt.Parse(now.Sub(t.UpdatedAt)).LimitFirstN(2).String(), - "ExpiresIn": durafmt.Parse(now.Add(maxTime).Sub(now)).LimitFirstN(2).String(), - "Token": t.Token, - "Ip": t.Ip, - }) - } - - return e.Render(200, "account.html", map[string]any{ - "Repo": repo, - "Tokens": tokenInfo, - "flashes": getFlashesFromSession(e, sess), - }) -} diff --git a/server/handle_account_revoke.go b/server/handle_account_revoke.go deleted file mode 100644 index c392fc1..0000000 --- a/server/handle_account_revoke.go +++ /dev/null @@ -1,37 +0,0 @@ -package server - -import ( - "github.com/haileyok/cocoon/internal/helpers" - "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) - } - - repo, sess, err := s.getSessionRepoOrErr(e) - if err != 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") - } - - sess.AddFlash("Session successfully revoked!", "success") - sess.Save(e.Request(), e.Response()) - return e.Redirect(303, "/account") -} diff --git a/server/handle_account_signin.go b/server/handle_account_signin.go deleted file mode 100644 index 4490426..0000000 --- a/server/handle_account_signin.go +++ /dev/null @@ -1,182 +0,0 @@ -package server - -import ( - "errors" - "fmt" - "strings" - "time" - - "github.com/bluesky-social/indigo/atproto/syntax" - "github.com/gorilla/sessions" - "github.com/haileyok/cocoon/internal/helpers" - "github.com/haileyok/cocoon/models" - "github.com/labstack/echo-contrib/session" - "github.com/labstack/echo/v4" - "golang.org/x/crypto/bcrypt" - "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(s.config.SessionCookieKey, e) - if err != nil { - return nil, nil, err - } - - did, ok := sess.Values["did"].(string) - if !ok { - return nil, sess, errors.New("did was not set in session") - } - - repo, err := s.getRepoActorByDid(ctx, did) - if err != nil { - return nil, sess, err - } - - return repo, sess, nil -} - -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) handleAccountSigninGet(e echo.Context) error { - _, sess, err := s.getSessionRepoOrErr(e) - if err == nil { - return e.Redirect(303, "/account") - } - - return e.Render(200, "signin.html", map[string]any{ - "flashes": getFlashesFromSession(e, sess), - "QueryParams": e.QueryParams().Encode(), - }) -} - -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) - } - - sess, _ := session.Get(s.config.SessionCookieKey, e) - - req.Username = strings.ToLower(req.Username) - var idtype string - if _, err := syntax.ParseDID(req.Username); err == nil { - idtype = "did" - } else if _, err := syntax.ParseHandle(req.Username); err == 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 repo models.RepoActor - 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("Handle or password is incorrect", "error") - } else { - 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 { - if err != bcrypt.ErrMismatchedHashAndPassword { - sess.AddFlash("Handle or password is incorrect", "error") - } else { - 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{ - Path: "/", - MaxAge: int(AccountSessionMaxAge.Seconds()), - HttpOnly: true, - } - - sess.Values = map[any]any{} - sess.Values["did"] = repo.Repo.Did - - if err := sess.Save(e.Request(), e.Response()); err != nil { - return err - } - - if queryParams != "" { - return e.Redirect(303, "/oauth/authorize"+queryParams) - } else { - return e.Redirect(303, "/account") - } -} diff --git a/server/handle_account_signout.go b/server/handle_account_signout.go deleted file mode 100644 index 48e7671..0000000 --- a/server/handle_account_signout.go +++ /dev/null @@ -1,35 +0,0 @@ -package server - -import ( - "github.com/gorilla/sessions" - "github.com/labstack/echo-contrib/session" - "github.com/labstack/echo/v4" -) - -func (s *Server) handleAccountSignout(e echo.Context) error { - sess, err := session.Get(s.config.SessionCookieKey, e) - if err != nil { - return err - } - - sess.Options = &sessions.Options{ - Path: "/", - MaxAge: -1, - HttpOnly: true, - } - - sess.Values = map[any]any{} - - if err := sess.Save(e.Request(), e.Response()); err != nil { - return err - } - - reqUri := e.QueryParam("request_uri") - - redirect := "/account/signin" - if reqUri != "" { - redirect += "?" + e.QueryParams().Encode() - } - - return e.Redirect(303, redirect) -} diff --git a/server/handle_actor_get_preferences.go b/server/handle_actor_get_preferences.go index 0407e8a..d49343d 100644 --- a/server/handle_actor_get_preferences.go +++ b/server/handle_actor_get_preferences.go @@ -3,7 +3,7 @@ package server import ( "encoding/json" - "github.com/haileyok/cocoon/models" + "tangled.org/pdewey.com/chrysalis/models" "github.com/labstack/echo/v4" ) diff --git a/server/handle_actor_put_preferences.go b/server/handle_actor_put_preferences.go index 2d2ed0e..3fef90e 100644 --- a/server/handle_actor_put_preferences.go +++ b/server/handle_actor_put_preferences.go @@ -3,7 +3,7 @@ package server import ( "encoding/json" - "github.com/haileyok/cocoon/models" + "tangled.org/pdewey.com/chrysalis/models" "github.com/labstack/echo/v4" ) diff --git a/server/handle_age_assurance.go b/server/handle_age_assurance.go index d4afc88..2d965ba 100644 --- a/server/handle_age_assurance.go +++ b/server/handle_age_assurance.go @@ -4,7 +4,7 @@ import ( "time" "github.com/bluesky-social/indigo/util" - "github.com/haileyok/cocoon/models" + "tangled.org/pdewey.com/chrysalis/models" "github.com/labstack/echo/v4" ) diff --git a/server/handle_health.go b/server/handle_health.go index 9bc118b..1e2849c 100644 --- a/server/handle_health.go +++ b/server/handle_health.go @@ -4,6 +4,6 @@ import "github.com/labstack/echo/v4" func (s *Server) handleHealth(e echo.Context) error { return e.JSON(200, map[string]string{ - "version": "cocoon " + s.config.Version, + "version": "chrysalis", }) } diff --git a/server/handle_identity_get_recommended_did_credentials.go b/server/handle_identity_get_recommended_did_credentials.go index d9c4360..23cadaa 100644 --- a/server/handle_identity_get_recommended_did_credentials.go +++ b/server/handle_identity_get_recommended_did_credentials.go @@ -2,8 +2,8 @@ package server import ( "github.com/bluesky-social/indigo/atproto/atcrypto" - "github.com/haileyok/cocoon/internal/helpers" - "github.com/haileyok/cocoon/models" + "tangled.org/pdewey.com/chrysalis/internal/helpers" + "tangled.org/pdewey.com/chrysalis/models" "github.com/labstack/echo/v4" ) diff --git a/server/handle_identity_request_plc_operation.go b/server/handle_identity_request_plc_operation.go index 16b0ba6..7cd04d9 100644 --- a/server/handle_identity_request_plc_operation.go +++ b/server/handle_identity_request_plc_operation.go @@ -4,8 +4,8 @@ import ( "fmt" "time" - "github.com/haileyok/cocoon/internal/helpers" - "github.com/haileyok/cocoon/models" + "tangled.org/pdewey.com/chrysalis/internal/helpers" + "tangled.org/pdewey.com/chrysalis/models" "github.com/labstack/echo/v4" ) diff --git a/server/handle_identity_sign_plc_operation.go b/server/handle_identity_sign_plc_operation.go index d6f29ee..9aca1fb 100644 --- a/server/handle_identity_sign_plc_operation.go +++ b/server/handle_identity_sign_plc_operation.go @@ -6,10 +6,10 @@ import ( "time" "github.com/bluesky-social/indigo/atproto/atcrypto" - "github.com/haileyok/cocoon/identity" - "github.com/haileyok/cocoon/internal/helpers" - "github.com/haileyok/cocoon/models" - "github.com/haileyok/cocoon/plc" + "tangled.org/pdewey.com/chrysalis/identity" + "tangled.org/pdewey.com/chrysalis/internal/helpers" + "tangled.org/pdewey.com/chrysalis/models" + "tangled.org/pdewey.com/chrysalis/plc" "github.com/labstack/echo/v4" ) diff --git a/server/handle_identity_submit_plc_operation.go b/server/handle_identity_submit_plc_operation.go index 0b23651..0a285f8 100644 --- a/server/handle_identity_submit_plc_operation.go +++ b/server/handle_identity_submit_plc_operation.go @@ -10,9 +10,9 @@ import ( "github.com/bluesky-social/indigo/atproto/atcrypto" "github.com/bluesky-social/indigo/events" "github.com/bluesky-social/indigo/util" - "github.com/haileyok/cocoon/internal/helpers" - "github.com/haileyok/cocoon/models" - "github.com/haileyok/cocoon/plc" + "tangled.org/pdewey.com/chrysalis/internal/helpers" + "tangled.org/pdewey.com/chrysalis/models" + "tangled.org/pdewey.com/chrysalis/plc" "github.com/labstack/echo/v4" ) diff --git a/server/handle_identity_update_handle.go b/server/handle_identity_update_handle.go index 26f8b97..faa6cd7 100644 --- a/server/handle_identity_update_handle.go +++ b/server/handle_identity_update_handle.go @@ -9,10 +9,10 @@ import ( "github.com/bluesky-social/indigo/atproto/atcrypto" "github.com/bluesky-social/indigo/events" "github.com/bluesky-social/indigo/util" - "github.com/haileyok/cocoon/identity" - "github.com/haileyok/cocoon/internal/helpers" - "github.com/haileyok/cocoon/models" - "github.com/haileyok/cocoon/plc" + "tangled.org/pdewey.com/chrysalis/identity" + "tangled.org/pdewey.com/chrysalis/internal/helpers" + "tangled.org/pdewey.com/chrysalis/models" + "tangled.org/pdewey.com/chrysalis/plc" "github.com/labstack/echo/v4" ) diff --git a/server/handle_import_repo.go b/server/handle_import_repo.go index 3643982..e126039 100644 --- a/server/handle_import_repo.go +++ b/server/handle_import_repo.go @@ -8,8 +8,8 @@ import ( "strings" "github.com/bluesky-social/indigo/atproto/syntax" - "github.com/haileyok/cocoon/internal/helpers" - "github.com/haileyok/cocoon/models" + "tangled.org/pdewey.com/chrysalis/internal/helpers" + "tangled.org/pdewey.com/chrysalis/models" blocks "github.com/ipfs/go-block-format" "github.com/ipfs/go-cid" "github.com/ipld/go-car" diff --git a/server/handle_label_query_labels.go b/server/handle_label_query_labels.go index 8e1c1d9..86c9b24 100644 --- a/server/handle_label_query_labels.go +++ b/server/handle_label_query_labels.go @@ -22,11 +22,6 @@ type ComAtprotoLabelQueryLabelsResponse struct { } func (s *Server) handleLabelQueryLabels(e echo.Context) error { - svc := e.Request().Header.Get("atproto-proxy") - if svc != "" || s.config.FallbackProxy != "" { - return s.handleProxy(e) - } - return e.JSON(200, ComAtprotoLabelQueryLabelsResponse{ Cursor: nil, Labels: []Label{}, diff --git a/server/handle_oauth_authorize.go b/server/handle_oauth_authorize.go deleted file mode 100644 index b42aac7..0000000 --- a/server/handle_oauth_authorize.go +++ /dev/null @@ -1,212 +0,0 @@ -package server - -import ( - "fmt" - "net/url" - "strings" - "time" - - "github.com/haileyok/cocoon/internal/helpers" - "github.com/haileyok/cocoon/oauth" - "github.com/haileyok/cocoon/oauth/constants" - "github.com/haileyok/cocoon/oauth/provider" - "github.com/labstack/echo/v4" -) - -type HandleOauthAuthorizeGetInput struct { - RequestUri string `query:"request_uri"` -} - -func (s *Server) handleOauthAuthorizeGet(e echo.Context) error { - ctx := e.Request().Context() - - logger := s.logger.With("name", "handleOauthAuthorizeGet") - - var input HandleOauthAuthorizeGetInput - if err := e.Bind(&input); err != nil { - logger.Error("error binding request", "err", err) - return fmt.Errorf("error binding request") - } - - var reqId string - if input.RequestUri != "" { - id, err := oauth.DecodeRequestUri(input.RequestUri) - if err != nil { - logger.Error("no request uri found in input", "url", e.Request().URL.String()) - return helpers.InputError(e, new("no request uri")) - } - reqId = id - } else { - var parRequest provider.ParRequest - if err := e.Bind(&parRequest); err != nil { - s.logger.Error("error binding for standard auth request", "error", err) - return helpers.InputError(e, new("InvalidRequest")) - } - - if err := e.Validate(parRequest); err != nil { - // render page for logged out dev - if s.config.Version == "dev" && parRequest.ClientID == "" { - return e.Render(200, "authorize.html", map[string]any{ - "Scopes": []string{"atproto", "transition:generic"}, - "AppName": "DEV MODE AUTHORIZATION PAGE", - "Handle": "paula.cocoon.social", - "RequestUri": "", - }) - } - return helpers.InputError(e, new("no request uri and invalid parameters")) - } - - client, clientAuth, err := s.oauthProvider.AuthenticateClient(ctx, parRequest.AuthenticateClientRequestBase, nil, &provider.AuthenticateClientOptions{ - AllowMissingDpopProof: true, - }) - if err != nil { - s.logger.Error("error authenticating client in standard request", "client_id", parRequest.ClientID, "error", err) - return helpers.ServerError(e, new(err.Error())) - } - - if parRequest.DpopJkt == nil { - if client.Metadata.DpopBoundAccessTokens { - } - } else { - if !client.Metadata.DpopBoundAccessTokens { - msg := "dpop bound access tokens are not enabled for this client" - return helpers.InputError(e, &msg) - } - } - - eat := time.Now().Add(constants.ParExpiresIn) - id := oauth.GenerateRequestId() - - authRequest := &provider.OauthAuthorizationRequest{ - RequestId: id, - ClientId: client.Metadata.ClientID, - ClientAuth: *clientAuth, - Parameters: parRequest, - ExpiresAt: eat, - } - - if err := s.db.Create(ctx, authRequest, nil).Error; err != nil { - s.logger.Error("error creating auth request in db", "error", err) - return helpers.ServerError(e, nil) - } - - input.RequestUri = oauth.EncodeRequestUri(id) - reqId = id - - } - - repo, _, err := s.getSessionRepoOrErr(e) - if err != nil { - return e.Redirect(303, "/account/signin?"+e.QueryParams().Encode()) - } - - 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, new(err.Error())) - } - - clientId := e.QueryParam("client_id") - if clientId != req.ClientId { - return helpers.InputError(e, new("client id does not match the client id for the supplied request")) - } - - client, err := s.oauthProvider.ClientManager.GetClient(e.Request().Context(), req.ClientId) - if err != nil { - return helpers.ServerError(e, new(err.Error())) - } - - scopes := strings.Split(req.Parameters.Scope, " ") - appName := client.Metadata.ClientName - - data := map[string]any{ - "Scopes": scopes, - "AppName": appName, - "RequestUri": input.RequestUri, - "QueryParams": e.QueryParams().Encode(), - "Handle": repo.Actor.Handle, - } - - return e.Render(200, "authorize.html", data) -} - -type OauthAuthorizePostRequest struct { - RequestUri string `form:"request_uri"` - AcceptOrRejct string `form:"accept_or_reject"` -} - -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) - } - - reqId, err := oauth.DecodeRequestUri(req.RequestUri) - if err != nil { - return helpers.InputError(e, new(err.Error())) - } - - 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, new(err.Error())) - } - - client, err := s.oauthProvider.ClientManager.GetClient(e.Request().Context(), authReq.ClientId) - if err != nil { - return helpers.ServerError(e, new(err.Error())) - } - - // TODO: figure out how im supposed to actually redirect - if req.AcceptOrRejct == "reject" { - return e.Redirect(303, client.Metadata.ClientURI) - } - - if time.Now().After(authReq.ExpiresAt) { - return helpers.InputError(e, new("the request has expired")) - } - - if authReq.Sub != nil || authReq.Code != nil { - return helpers.InputError(e, new("this request was already authorized")) - } - - 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) - } - - q := url.Values{} - q.Set("state", authReq.Parameters.State) - q.Set("iss", "https://"+s.config.Hostname) - q.Set("code", code) - - hashOrQuestion := "?" - if authReq.Parameters.ResponseMode != nil { - switch *authReq.Parameters.ResponseMode { - case "fragment": - hashOrQuestion = "#" - case "query": - // do nothing - break - default: - if authReq.Parameters.ResponseType != "code" { - hashOrQuestion = "#" - } - } - } else { - if authReq.Parameters.ResponseType != "code" { - hashOrQuestion = "#" - } - } - - return e.Redirect(303, authReq.Parameters.RedirectURI+hashOrQuestion+q.Encode()) -} diff --git a/server/handle_oauth_par.go b/server/handle_oauth_par.go index 622b1f1..4b012b0 100644 --- a/server/handle_oauth_par.go +++ b/server/handle_oauth_par.go @@ -4,11 +4,11 @@ import ( "errors" "time" - "github.com/haileyok/cocoon/internal/helpers" - "github.com/haileyok/cocoon/oauth" - "github.com/haileyok/cocoon/oauth/constants" - "github.com/haileyok/cocoon/oauth/dpop" - "github.com/haileyok/cocoon/oauth/provider" + "tangled.org/pdewey.com/chrysalis/internal/helpers" + "tangled.org/pdewey.com/chrysalis/oauth" + "tangled.org/pdewey.com/chrysalis/oauth/constants" + "tangled.org/pdewey.com/chrysalis/oauth/dpop" + "tangled.org/pdewey.com/chrysalis/oauth/provider" "github.com/labstack/echo/v4" ) diff --git a/server/handle_oauth_token.go b/server/handle_oauth_token.go index adbe2b7..f37bd27 100644 --- a/server/handle_oauth_token.go +++ b/server/handle_oauth_token.go @@ -10,11 +10,11 @@ import ( "time" "github.com/golang-jwt/jwt/v4" - "github.com/haileyok/cocoon/internal/helpers" - "github.com/haileyok/cocoon/oauth" - "github.com/haileyok/cocoon/oauth/constants" - "github.com/haileyok/cocoon/oauth/dpop" - "github.com/haileyok/cocoon/oauth/provider" + "tangled.org/pdewey.com/chrysalis/internal/helpers" + "tangled.org/pdewey.com/chrysalis/oauth" + "tangled.org/pdewey.com/chrysalis/oauth/constants" + "tangled.org/pdewey.com/chrysalis/oauth/dpop" + "tangled.org/pdewey.com/chrysalis/oauth/provider" "github.com/labstack/echo/v4" ) diff --git a/server/handle_proxy.go b/server/handle_proxy.go deleted file mode 100644 index 5a56ea9..0000000 --- a/server/handle_proxy.go +++ /dev/null @@ -1,169 +0,0 @@ -package server - -import ( - "crypto/rand" - "crypto/sha256" - "encoding/base64" - "encoding/json" - "fmt" - "net/http" - "strings" - "time" - - "github.com/google/uuid" - "github.com/haileyok/cocoon/internal/helpers" - "github.com/haileyok/cocoon/models" - "github.com/labstack/echo/v4" - secp256k1secec "gitlab.com/yawning/secp256k1-voi/secec" -) - -func (s *Server) getAtprotoProxyEndpointFromRequest(e echo.Context) (string, string, error) { - svc := e.Request().Header.Get("atproto-proxy") - if svc == "" && s.config.FallbackProxy != "" { - svc = s.config.FallbackProxy - } - - svcPts := strings.Split(svc, "#") - if len(svcPts) != 2 { - return "", "", fmt.Errorf("invalid service header") - } - - svcDid := svcPts[0] - svcId := "#" + svcPts[1] - - doc, err := s.passport.FetchDoc(e.Request().Context(), svcDid) - if err != nil { - return "", "", err - } - - var endpoint string - for _, s := range doc.Service { - if s.Id == svcId { - endpoint = s.ServiceEndpoint - } - } - - return endpoint, svcDid, nil -} - -func (s *Server) handleProxy(e echo.Context) error { - logger := s.logger.With("handler", "handleProxy") - - repo, isAuthed := e.Get("repo").(*models.RepoActor) - - pts := strings.Split(e.Request().URL.Path, "/") - if len(pts) != 3 { - return fmt.Errorf("incorrect number of parts") - } - - endpoint, svcDid, err := s.getAtprotoProxyEndpointFromRequest(e) - if err != nil { - logger.Error("could not get atproto proxy", "error", err) - return helpers.ServerError(e, nil) - } - - requrl := e.Request().URL - requrl.Host = strings.TrimPrefix(endpoint, "https://") - requrl.Scheme = "https" - - body := e.Request().Body - if e.Request().Method == "GET" { - body = nil - } - - req, err := http.NewRequest(e.Request().Method, requrl.String(), body) - if err != nil { - return err - } - - req.Header = e.Request().Header.Clone() - - if isAuthed { - // this is a little dumb. i should probably figure out a better way to do this, and use - // a single way of creating/signing jwts throughout the pds. kinda limited here because - // im using the atproto crypto lib for this though. will come back to it - - header := map[string]string{ - "alg": "ES256K", - "crv": "secp256k1", - "typ": "JWT", - } - hj, err := json.Marshal(header) - if err != nil { - logger.Error("error marshaling header", "error", err) - return helpers.ServerError(e, nil) - } - - encheader := strings.TrimRight(base64.RawURLEncoding.EncodeToString(hj), "=") - - // When proxying app.bsky.feed.getFeed the token is actually issued for the - // underlying feed generator and the app view passes it on. This allows the - // getFeed implementation to pass in the desired lxm and aud for the token - // and then just delegate to the general proxying logic - lxm, proxyTokenLxmExists := e.Get("proxyTokenLxm").(string) - if !proxyTokenLxmExists || lxm == "" { - lxm = pts[2] - } - aud, proxyTokenAudExists := e.Get("proxyTokenAud").(string) - if !proxyTokenAudExists || aud == "" { - aud = svcDid - } - - payload := map[string]any{ - "iss": repo.Repo.Did, - "aud": aud, - "lxm": lxm, - "jti": uuid.NewString(), - "exp": time.Now().Add(1 * time.Minute).UTC().Unix(), - } - pj, err := json.Marshal(payload) - if err != nil { - logger.Error("error marashaling payload", "error", err) - return helpers.ServerError(e, nil) - } - - encpayload := strings.TrimRight(base64.RawURLEncoding.EncodeToString(pj), "=") - - input := fmt.Sprintf("%s.%s", encheader, encpayload) - hash := sha256.Sum256([]byte(input)) - - 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() - sBytes := S.Bytes() - - rPadded := make([]byte, 32) - sPadded := make([]byte, 32) - copy(rPadded[32-len(rBytes):], rBytes) - copy(sPadded[32-len(sBytes):], sBytes) - - rawsig := append(rPadded, sPadded...) - encsig := strings.TrimRight(base64.RawURLEncoding.EncodeToString(rawsig), "=") - token := fmt.Sprintf("%s.%s", input, encsig) - - req.Header.Set("authorization", "Bearer "+token) - } else { - req.Header.Del("authorization") - } - - resp, err := http.DefaultClient.Do(req) - if err != nil { - return err - } - defer resp.Body.Close() - - for k, v := range resp.Header { - e.Response().Header().Set(k, strings.Join(v, ",")) - } - - return e.Stream(resp.StatusCode, e.Response().Header().Get("content-type"), resp.Body) -} diff --git a/server/handle_proxy_get_feed.go b/server/handle_proxy_get_feed.go deleted file mode 100644 index 1568aa0..0000000 --- a/server/handle_proxy_get_feed.go +++ /dev/null @@ -1,34 +0,0 @@ -package server - -import ( - "github.com/bluesky-social/indigo/api/atproto" - "github.com/bluesky-social/indigo/api/bsky" - "github.com/bluesky-social/indigo/atproto/syntax" - "github.com/bluesky-social/indigo/xrpc" - "github.com/haileyok/cocoon/internal/helpers" - "github.com/labstack/echo/v4" -) - -func (s *Server) handleProxyBskyFeedGetFeed(e echo.Context) error { - feedUri, err := syntax.ParseATURI(e.QueryParam("feed")) - if err != nil { - return helpers.InputError(e, new("invalid feed uri")) - } - - appViewEndpoint, _, err := s.getAtprotoProxyEndpointFromRequest(e) - if err != nil { - e.Logger().Error("could not get atproto proxy", "error", err) - return helpers.ServerError(e, nil) - } - - appViewClient := xrpc.Client{ - Host: appViewEndpoint, - } - feedRecord, err := atproto.RepoGetRecord(e.Request().Context(), &appViewClient, "", feedUri.Collection().String(), feedUri.Authority().String(), feedUri.RecordKey().String()) - feedGeneratorDid := feedRecord.Value.Val.(*bsky.FeedGenerator).Did - - e.Set("proxyTokenLxm", "app.bsky.feed.getFeedSkeleton") - e.Set("proxyTokenAud", feedGeneratorDid) - - return s.handleProxy(e) -} diff --git a/server/handle_repo_apply_writes.go b/server/handle_repo_apply_writes.go index 479e72b..4f48fe9 100644 --- a/server/handle_repo_apply_writes.go +++ b/server/handle_repo_apply_writes.go @@ -1,8 +1,8 @@ package server import ( - "github.com/haileyok/cocoon/internal/helpers" - "github.com/haileyok/cocoon/models" + "tangled.org/pdewey.com/chrysalis/internal/helpers" + "tangled.org/pdewey.com/chrysalis/models" "github.com/labstack/echo/v4" ) diff --git a/server/handle_repo_create_record.go b/server/handle_repo_create_record.go index ed2f5e2..d76bfb7 100644 --- a/server/handle_repo_create_record.go +++ b/server/handle_repo_create_record.go @@ -1,8 +1,8 @@ package server import ( - "github.com/haileyok/cocoon/internal/helpers" - "github.com/haileyok/cocoon/models" + "tangled.org/pdewey.com/chrysalis/internal/helpers" + "tangled.org/pdewey.com/chrysalis/models" "github.com/labstack/echo/v4" ) diff --git a/server/handle_repo_delete_record.go b/server/handle_repo_delete_record.go index 3e60562..ff47e5c 100644 --- a/server/handle_repo_delete_record.go +++ b/server/handle_repo_delete_record.go @@ -1,8 +1,8 @@ package server import ( - "github.com/haileyok/cocoon/internal/helpers" - "github.com/haileyok/cocoon/models" + "tangled.org/pdewey.com/chrysalis/internal/helpers" + "tangled.org/pdewey.com/chrysalis/models" "github.com/labstack/echo/v4" ) diff --git a/server/handle_repo_describe_repo.go b/server/handle_repo_describe_repo.go index 487d5c1..f6c1c22 100644 --- a/server/handle_repo_describe_repo.go +++ b/server/handle_repo_describe_repo.go @@ -3,9 +3,9 @@ package server import ( "strings" - "github.com/haileyok/cocoon/identity" - "github.com/haileyok/cocoon/internal/helpers" - "github.com/haileyok/cocoon/models" + "tangled.org/pdewey.com/chrysalis/identity" + "tangled.org/pdewey.com/chrysalis/internal/helpers" + "tangled.org/pdewey.com/chrysalis/models" "github.com/labstack/echo/v4" "gorm.io/gorm" ) diff --git a/server/handle_repo_get_record.go b/server/handle_repo_get_record.go index 5aae9f5..b264a70 100644 --- a/server/handle_repo_get_record.go +++ b/server/handle_repo_get_record.go @@ -3,8 +3,9 @@ package server import ( "github.com/bluesky-social/indigo/atproto/atdata" "github.com/bluesky-social/indigo/atproto/syntax" - "github.com/haileyok/cocoon/models" "github.com/labstack/echo/v4" + "tangled.org/pdewey.com/chrysalis/internal/helpers" + "tangled.org/pdewey.com/chrysalis/models" ) type ComAtprotoRepoGetRecordResponse struct { @@ -41,7 +42,7 @@ func (s *Server) handleRepoGetRecord(e echo.Context) error { val, err := atdata.UnmarshalCBOR(record.Value) if err != nil { - return s.handleProxy(e) // TODO: this should be getting handled like...if we don't find it in the db. why doesn't it throw error up there? + return helpers.ServerError(e, nil) } return e.JSON(200, ComAtprotoRepoGetRecordResponse{ diff --git a/server/handle_repo_list_missing_blobs.go b/server/handle_repo_list_missing_blobs.go index 5efe38d..9cb4d2d 100644 --- a/server/handle_repo_list_missing_blobs.go +++ b/server/handle_repo_list_missing_blobs.go @@ -5,8 +5,8 @@ import ( "strconv" "github.com/bluesky-social/indigo/atproto/atdata" - "github.com/haileyok/cocoon/internal/helpers" - "github.com/haileyok/cocoon/models" + "tangled.org/pdewey.com/chrysalis/internal/helpers" + "tangled.org/pdewey.com/chrysalis/models" "github.com/ipfs/go-cid" "github.com/labstack/echo/v4" ) diff --git a/server/handle_repo_list_records.go b/server/handle_repo_list_records.go index bfae33e..29a694a 100644 --- a/server/handle_repo_list_records.go +++ b/server/handle_repo_list_records.go @@ -5,8 +5,8 @@ import ( "github.com/bluesky-social/indigo/atproto/atdata" "github.com/bluesky-social/indigo/atproto/syntax" - "github.com/haileyok/cocoon/internal/helpers" - "github.com/haileyok/cocoon/models" + "tangled.org/pdewey.com/chrysalis/internal/helpers" + "tangled.org/pdewey.com/chrysalis/models" "github.com/labstack/echo/v4" ) diff --git a/server/handle_repo_list_repos.go b/server/handle_repo_list_repos.go index d51b3cd..7f7edea 100644 --- a/server/handle_repo_list_repos.go +++ b/server/handle_repo_list_repos.go @@ -1,7 +1,7 @@ package server import ( - "github.com/haileyok/cocoon/models" + "tangled.org/pdewey.com/chrysalis/models" "github.com/ipfs/go-cid" "github.com/labstack/echo/v4" ) diff --git a/server/handle_repo_put_record.go b/server/handle_repo_put_record.go index 5c3e11e..d58d555 100644 --- a/server/handle_repo_put_record.go +++ b/server/handle_repo_put_record.go @@ -1,8 +1,8 @@ package server import ( - "github.com/haileyok/cocoon/internal/helpers" - "github.com/haileyok/cocoon/models" + "tangled.org/pdewey.com/chrysalis/internal/helpers" + "tangled.org/pdewey.com/chrysalis/models" "github.com/labstack/echo/v4" ) diff --git a/server/handle_repo_upload_blob.go b/server/handle_repo_upload_blob.go index 321189f..8aeb055 100644 --- a/server/handle_repo_upload_blob.go +++ b/server/handle_repo_upload_blob.go @@ -2,15 +2,10 @@ package server import ( "bytes" - "fmt" "io" - "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/aws/credentials" - "github.com/aws/aws-sdk-go/aws/session" - "github.com/aws/aws-sdk-go/service/s3" - "github.com/haileyok/cocoon/internal/helpers" - "github.com/haileyok/cocoon/models" + "tangled.org/pdewey.com/chrysalis/internal/helpers" + "tangled.org/pdewey.com/chrysalis/models" "github.com/ipfs/go-cid" "github.com/labstack/echo/v4" "github.com/multiformats/go-multihash" @@ -42,16 +37,11 @@ func (s *Server) handleRepoUploadBlob(e echo.Context) error { mime = "application/octet-stream" } - storage := "sqlite" - s3Upload := s.s3Config != nil && s.s3Config.BlobstoreEnabled - if s3Upload { - storage = "s3" - } blob := models.Blob{ Did: urepo.Repo.Did, RefCount: 0, CreatedAt: s.repoman.clock.Next().String(), - Storage: storage, + Storage: "sqlite", } if err := s.db.Create(ctx, &blob, nil).Error; err != nil { @@ -80,17 +70,15 @@ func (s *Server) handleRepoUploadBlob(e echo.Context) error { read += n fulldata.Write(data) - if !s3Upload { - blobPart := models.BlobPart{ - BlobID: blob.ID, - Idx: part, - Data: data, - } + blobPart := models.BlobPart{ + BlobID: blob.ID, + Idx: part, + 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) - } + 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) } part++ @@ -105,37 +93,7 @@ func (s *Server) handleRepoUploadBlob(e echo.Context) error { return helpers.ServerError(e, nil) } - if s3Upload { - config := &aws.Config{ - Region: new(s.s3Config.Region), - Credentials: credentials.NewStaticCredentials(s.s3Config.AccessKey, s.s3Config.SecretKey, ""), - } - - if s.s3Config.Endpoint != "" { - config.Endpoint = new(s.s3Config.Endpoint) - config.S3ForcePathStyle = new(true) - } - - sess, err := session.NewSession(config) - if err != nil { - logger.Error("error creating aws session", "error", err) - return helpers.ServerError(e, nil) - } - - svc := s3.New(sess) - - if _, err := svc.PutObject(&s3.PutObjectInput{ - Bucket: new(s.s3Config.Bucket), - Key: new(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) } diff --git a/server/handle_root.go b/server/handle_root.go deleted file mode 100644 index 80ec6d1..0000000 --- a/server/handle_root.go +++ /dev/null @@ -1,40 +0,0 @@ -package server - -import "github.com/labstack/echo/v4" - -func (s *Server) handleRoot(e echo.Context) error { - return e.String(200, ` - - ....-*%%%##### - .%#+++****#%%%%%%%%%#+:.... - .%+++**++++*%%%%..... - .%+++*****#%%%%#.. %#%... - ***+*****%%%%%... =.. - *****%%%%.. +=++.. - %%%%%... .+----==++. - .-::----===++ - .=-:.------==+++ - +-:::-:----===++.. - =-::-----:-==+++-. - .==*=------==++++. - +-:--=++===*=--++. - +:::--:=++=----=+.. - *::::---=+#----=+. - =::::----=+#---=+.. - .::::----==+=--=+.. - .-::-----==++=-=+.. - -::-----==++===+.. - =::-----==++==++ - +::----:==++=+++ - :-:----:==+++++. - .=:=----=+++++. - +=-=====+++.. - =====++. - =++... - - -This is an AT Protocol Personal Data Server (aka, an atproto PDS) - -Code: https://github.com/haileyok/cocoon -Version: `+s.config.Version+"\n") -} diff --git a/server/handle_server_activate_account.go b/server/handle_server_activate_account.go index 7db1aa2..7b46116 100644 --- a/server/handle_server_activate_account.go +++ b/server/handle_server_activate_account.go @@ -7,8 +7,8 @@ import ( "github.com/bluesky-social/indigo/api/atproto" "github.com/bluesky-social/indigo/events" "github.com/bluesky-social/indigo/util" - "github.com/haileyok/cocoon/internal/helpers" - "github.com/haileyok/cocoon/models" + "tangled.org/pdewey.com/chrysalis/internal/helpers" + "tangled.org/pdewey.com/chrysalis/models" "github.com/labstack/echo/v4" ) diff --git a/server/handle_server_check_account_status.go b/server/handle_server_check_account_status.go index 5092451..bdf55e9 100644 --- a/server/handle_server_check_account_status.go +++ b/server/handle_server_check_account_status.go @@ -1,8 +1,8 @@ package server import ( - "github.com/haileyok/cocoon/internal/helpers" - "github.com/haileyok/cocoon/models" + "tangled.org/pdewey.com/chrysalis/internal/helpers" + "tangled.org/pdewey.com/chrysalis/models" "github.com/ipfs/go-cid" "github.com/labstack/echo/v4" ) diff --git a/server/handle_server_confirm_email.go b/server/handle_server_confirm_email.go index 4e0b165..bff5551 100644 --- a/server/handle_server_confirm_email.go +++ b/server/handle_server_confirm_email.go @@ -3,8 +3,8 @@ package server import ( "time" - "github.com/haileyok/cocoon/internal/helpers" - "github.com/haileyok/cocoon/models" + "tangled.org/pdewey.com/chrysalis/internal/helpers" + "tangled.org/pdewey.com/chrysalis/models" "github.com/labstack/echo/v4" ) diff --git a/server/handle_server_create_account.go b/server/handle_server_create_account.go index 8850336..8c0532e 100644 --- a/server/handle_server_create_account.go +++ b/server/handle_server_create_account.go @@ -14,8 +14,8 @@ import ( "github.com/bluesky-social/indigo/atproto/syntax" "github.com/bluesky-social/indigo/events" "github.com/bluesky-social/indigo/util" - "github.com/haileyok/cocoon/internal/helpers" - "github.com/haileyok/cocoon/models" + "tangled.org/pdewey.com/chrysalis/internal/helpers" + "tangled.org/pdewey.com/chrysalis/models" "github.com/labstack/echo/v4" "golang.org/x/crypto/bcrypt" "gorm.io/gorm" diff --git a/server/handle_server_create_invite_code.go b/server/handle_server_create_invite_code.go index 9909688..1b5177c 100644 --- a/server/handle_server_create_invite_code.go +++ b/server/handle_server_create_invite_code.go @@ -2,8 +2,8 @@ package server import ( "github.com/google/uuid" - "github.com/haileyok/cocoon/internal/helpers" - "github.com/haileyok/cocoon/models" + "tangled.org/pdewey.com/chrysalis/internal/helpers" + "tangled.org/pdewey.com/chrysalis/models" "github.com/labstack/echo/v4" ) diff --git a/server/handle_server_create_invite_codes.go b/server/handle_server_create_invite_codes.go index 8bd31ba..1f3085e 100644 --- a/server/handle_server_create_invite_codes.go +++ b/server/handle_server_create_invite_codes.go @@ -2,8 +2,8 @@ package server import ( "github.com/google/uuid" - "github.com/haileyok/cocoon/internal/helpers" - "github.com/haileyok/cocoon/models" + "tangled.org/pdewey.com/chrysalis/internal/helpers" + "tangled.org/pdewey.com/chrysalis/models" "github.com/labstack/echo/v4" ) diff --git a/server/handle_server_create_session.go b/server/handle_server_create_session.go index 68e7dbe..854a2af 100644 --- a/server/handle_server_create_session.go +++ b/server/handle_server_create_session.go @@ -8,8 +8,8 @@ import ( "time" "github.com/bluesky-social/indigo/atproto/syntax" - "github.com/haileyok/cocoon/internal/helpers" - "github.com/haileyok/cocoon/models" + "tangled.org/pdewey.com/chrysalis/internal/helpers" + "tangled.org/pdewey.com/chrysalis/models" "github.com/labstack/echo/v4" "golang.org/x/crypto/bcrypt" "gorm.io/gorm" diff --git a/server/handle_server_deactivate_account.go b/server/handle_server_deactivate_account.go index 31f3880..22c1de3 100644 --- a/server/handle_server_deactivate_account.go +++ b/server/handle_server_deactivate_account.go @@ -7,8 +7,8 @@ import ( "github.com/bluesky-social/indigo/api/atproto" "github.com/bluesky-social/indigo/events" "github.com/bluesky-social/indigo/util" - "github.com/haileyok/cocoon/internal/helpers" - "github.com/haileyok/cocoon/models" + "tangled.org/pdewey.com/chrysalis/internal/helpers" + "tangled.org/pdewey.com/chrysalis/models" "github.com/labstack/echo/v4" ) diff --git a/server/handle_server_delete_account.go b/server/handle_server_delete_account.go index beb1546..cead738 100644 --- a/server/handle_server_delete_account.go +++ b/server/handle_server_delete_account.go @@ -7,7 +7,7 @@ import ( "github.com/bluesky-social/indigo/api/atproto" "github.com/bluesky-social/indigo/events" "github.com/bluesky-social/indigo/util" - "github.com/haileyok/cocoon/internal/helpers" + "tangled.org/pdewey.com/chrysalis/internal/helpers" "github.com/labstack/echo/v4" "golang.org/x/crypto/bcrypt" ) diff --git a/server/handle_server_delete_session.go b/server/handle_server_delete_session.go index 7f2d5ba..351eb37 100644 --- a/server/handle_server_delete_session.go +++ b/server/handle_server_delete_session.go @@ -1,8 +1,8 @@ package server import ( - "github.com/haileyok/cocoon/internal/helpers" - "github.com/haileyok/cocoon/models" + "tangled.org/pdewey.com/chrysalis/internal/helpers" + "tangled.org/pdewey.com/chrysalis/models" "github.com/labstack/echo/v4" ) diff --git a/server/handle_server_get_service_auth.go b/server/handle_server_get_service_auth.go index 61e54b7..a8314e2 100644 --- a/server/handle_server_get_service_auth.go +++ b/server/handle_server_get_service_auth.go @@ -10,8 +10,8 @@ import ( "time" "github.com/google/uuid" - "github.com/haileyok/cocoon/internal/helpers" - "github.com/haileyok/cocoon/models" + "tangled.org/pdewey.com/chrysalis/internal/helpers" + "tangled.org/pdewey.com/chrysalis/models" "github.com/labstack/echo/v4" secp256k1secec "gitlab.com/yawning/secp256k1-voi/secec" ) diff --git a/server/handle_server_get_session.go b/server/handle_server_get_session.go index a5e41e6..cbf5c6a 100644 --- a/server/handle_server_get_session.go +++ b/server/handle_server_get_session.go @@ -1,7 +1,7 @@ package server import ( - "github.com/haileyok/cocoon/models" + "tangled.org/pdewey.com/chrysalis/models" "github.com/labstack/echo/v4" ) diff --git a/server/handle_server_refresh_session.go b/server/handle_server_refresh_session.go index c3efc4a..22968bd 100644 --- a/server/handle_server_refresh_session.go +++ b/server/handle_server_refresh_session.go @@ -1,8 +1,8 @@ package server import ( - "github.com/haileyok/cocoon/internal/helpers" - "github.com/haileyok/cocoon/models" + "tangled.org/pdewey.com/chrysalis/internal/helpers" + "tangled.org/pdewey.com/chrysalis/models" "github.com/labstack/echo/v4" ) diff --git a/server/handle_server_request_account_delete.go b/server/handle_server_request_account_delete.go index 53494e7..617628e 100644 --- a/server/handle_server_request_account_delete.go +++ b/server/handle_server_request_account_delete.go @@ -4,8 +4,8 @@ import ( "fmt" "time" - "github.com/haileyok/cocoon/internal/helpers" - "github.com/haileyok/cocoon/models" + "tangled.org/pdewey.com/chrysalis/internal/helpers" + "tangled.org/pdewey.com/chrysalis/models" "github.com/labstack/echo/v4" ) @@ -23,30 +23,5 @@ func (s *Server) handleServerRequestAccountDelete(e echo.Context) error { 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) - } - } - return e.NoContent(200) } - -func (s *Server) sendAccountDeleteEmail(email, handle, token string) error { - if s.mail == nil { - return nil - } - - s.mailLk.Lock() - defer s.mailLk.Unlock() - - s.mail.To(email) - s.mail.Subject("Account Deletion Request for " + s.config.Hostname) - s.mail.Plain().Set(fmt.Sprintf("Hello %s. Your account deletion code is %s. This code will expire in fifteen minutes. If you did not request this, please ignore this email.", handle, token)) - - if err := s.mail.Send(); err != nil { - return err - } - - return nil -} diff --git a/server/handle_server_request_email_confirmation.go b/server/handle_server_request_email_confirmation.go index cbe7e89..75f819a 100644 --- a/server/handle_server_request_email_confirmation.go +++ b/server/handle_server_request_email_confirmation.go @@ -4,8 +4,8 @@ import ( "fmt" "time" - "github.com/haileyok/cocoon/internal/helpers" - "github.com/haileyok/cocoon/models" + "tangled.org/pdewey.com/chrysalis/internal/helpers" + "tangled.org/pdewey.com/chrysalis/models" "github.com/labstack/echo/v4" ) diff --git a/server/handle_server_request_email_update.go b/server/handle_server_request_email_update.go index 85e8efe..d5dd404 100644 --- a/server/handle_server_request_email_update.go +++ b/server/handle_server_request_email_update.go @@ -4,8 +4,8 @@ import ( "fmt" "time" - "github.com/haileyok/cocoon/internal/helpers" - "github.com/haileyok/cocoon/models" + "tangled.org/pdewey.com/chrysalis/internal/helpers" + "tangled.org/pdewey.com/chrysalis/models" "github.com/labstack/echo/v4" ) diff --git a/server/handle_server_request_password_reset.go b/server/handle_server_request_password_reset.go index 6149ee2..9212c68 100644 --- a/server/handle_server_request_password_reset.go +++ b/server/handle_server_request_password_reset.go @@ -4,8 +4,8 @@ import ( "fmt" "time" - "github.com/haileyok/cocoon/internal/helpers" - "github.com/haileyok/cocoon/models" + "tangled.org/pdewey.com/chrysalis/internal/helpers" + "tangled.org/pdewey.com/chrysalis/models" "github.com/labstack/echo/v4" ) diff --git a/server/handle_server_reserve_signing_key.go b/server/handle_server_reserve_signing_key.go index d35d1c1..092f912 100644 --- a/server/handle_server_reserve_signing_key.go +++ b/server/handle_server_reserve_signing_key.go @@ -5,8 +5,8 @@ import ( "time" "github.com/bluesky-social/indigo/atproto/atcrypto" - "github.com/haileyok/cocoon/internal/helpers" - "github.com/haileyok/cocoon/models" + "tangled.org/pdewey.com/chrysalis/internal/helpers" + "tangled.org/pdewey.com/chrysalis/models" "github.com/labstack/echo/v4" ) diff --git a/server/handle_server_reset_password.go b/server/handle_server_reset_password.go index 152d93e..fc2ea28 100644 --- a/server/handle_server_reset_password.go +++ b/server/handle_server_reset_password.go @@ -3,8 +3,8 @@ package server import ( "time" - "github.com/haileyok/cocoon/internal/helpers" - "github.com/haileyok/cocoon/models" + "tangled.org/pdewey.com/chrysalis/internal/helpers" + "tangled.org/pdewey.com/chrysalis/models" "github.com/labstack/echo/v4" "golang.org/x/crypto/bcrypt" ) diff --git a/server/handle_server_resolve_handle.go b/server/handle_server_resolve_handle.go index f7e4a9d..620df39 100644 --- a/server/handle_server_resolve_handle.go +++ b/server/handle_server_resolve_handle.go @@ -4,7 +4,7 @@ import ( "context" "github.com/bluesky-social/indigo/atproto/syntax" - "github.com/haileyok/cocoon/internal/helpers" + "tangled.org/pdewey.com/chrysalis/internal/helpers" "github.com/labstack/echo/v4" ) diff --git a/server/handle_server_update_email.go b/server/handle_server_update_email.go index 481f014..52bfe0e 100644 --- a/server/handle_server_update_email.go +++ b/server/handle_server_update_email.go @@ -3,8 +3,8 @@ package server import ( "time" - "github.com/haileyok/cocoon/internal/helpers" - "github.com/haileyok/cocoon/models" + "tangled.org/pdewey.com/chrysalis/internal/helpers" + "tangled.org/pdewey.com/chrysalis/models" "github.com/labstack/echo/v4" ) diff --git a/server/handle_sync_get_blob.go b/server/handle_sync_get_blob.go index 7ed84c6..7ca012b 100644 --- a/server/handle_sync_get_blob.go +++ b/server/handle_sync_get_blob.go @@ -2,15 +2,9 @@ package server import ( "bytes" - "fmt" - "io" - "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/aws/credentials" - "github.com/aws/aws-sdk-go/aws/session" - "github.com/aws/aws-sdk-go/service/s3" - "github.com/haileyok/cocoon/internal/helpers" - "github.com/haileyok/cocoon/models" + "tangled.org/pdewey.com/chrysalis/internal/helpers" + "tangled.org/pdewey.com/chrysalis/models" "github.com/ipfs/go-cid" "github.com/labstack/echo/v4" ) @@ -53,82 +47,17 @@ func (s *Server) handleSyncGetBlob(e echo.Context) error { return helpers.ServerError(e, nil) } - buf := new(bytes.Buffer) - - 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) - } - - // TODO: we can just stream this, don't need to make a buffer - for _, p := range parts { - buf.Write(p.Data) - } - } else if blob.Storage == "s3" { - if !(s.s3Config != nil && s.s3Config.BlobstoreEnabled) { - logger.Error("s3 storage disabled") - return helpers.ServerError(e, nil) - } - - blobKey := fmt.Sprintf("blobs/%s/%s", urepo.Repo.Did, c.String()) - - if s.s3Config.CDNUrl != "" { - redirectUrl := fmt.Sprintf("%s/%s", s.s3Config.CDNUrl, blobKey) - return e.Redirect(302, redirectUrl) - } - - config := &aws.Config{ - Region: new(s.s3Config.Region), - Credentials: credentials.NewStaticCredentials(s.s3Config.AccessKey, s.s3Config.SecretKey, ""), - } - - if s.s3Config.Endpoint != "" { - config.Endpoint = new(s.s3Config.Endpoint) - config.S3ForcePathStyle = new(true) - } - - sess, err := session.NewSession(config) - if err != nil { - logger.Error("error creating aws session", "error", err) - return helpers.ServerError(e, nil) - } - - svc := s3.New(sess) - if result, err := svc.GetObject(&s3.GetObjectInput{ - Bucket: new(s.s3Config.Bucket), - Key: new(blobKey), - }); err != nil { - logger.Error("error getting blob from s3", "error", err) - return helpers.ServerError(e, nil) - } else { - read := 0 - part := 0 - partBuf := make([]byte, 0x10000) - - for { - n, err := io.ReadFull(result.Body, partBuf) - if err == io.ErrUnexpectedEOF || err == io.EOF { - if n == 0 { - break - } - } else if err != nil && err != io.ErrUnexpectedEOF { - logger.Error("error reading blob", "error", err) - return helpers.ServerError(e, nil) - } - - data := partBuf[:n] - read += n - buf.Write(data) - part++ - } - } - } else { - logger.Error("unknown storage", "storage", blob.Storage) + 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) } + buf := new(bytes.Buffer) + for _, p := range parts { + buf.Write(p.Data) + } + e.Response().Header().Set(echo.HeaderContentDisposition, "attachment; filename="+c.String()) return e.Stream(200, "application/octet-stream", buf) diff --git a/server/handle_sync_get_blocks.go b/server/handle_sync_get_blocks.go index 1577922..cca2aa7 100644 --- a/server/handle_sync_get_blocks.go +++ b/server/handle_sync_get_blocks.go @@ -4,7 +4,7 @@ import ( "bytes" "github.com/bluesky-social/indigo/carstore" - "github.com/haileyok/cocoon/internal/helpers" + "tangled.org/pdewey.com/chrysalis/internal/helpers" "github.com/ipfs/go-cid" cbor "github.com/ipfs/go-ipld-cbor" "github.com/ipld/go-car" diff --git a/server/handle_sync_get_latest_commit.go b/server/handle_sync_get_latest_commit.go index 176be68..e555587 100644 --- a/server/handle_sync_get_latest_commit.go +++ b/server/handle_sync_get_latest_commit.go @@ -1,7 +1,7 @@ package server import ( - "github.com/haileyok/cocoon/internal/helpers" + "tangled.org/pdewey.com/chrysalis/internal/helpers" "github.com/ipfs/go-cid" "github.com/labstack/echo/v4" ) diff --git a/server/handle_sync_get_record.go b/server/handle_sync_get_record.go index 613e476..54aee1d 100644 --- a/server/handle_sync_get_record.go +++ b/server/handle_sync_get_record.go @@ -4,8 +4,8 @@ import ( "bytes" "github.com/bluesky-social/indigo/carstore" - "github.com/haileyok/cocoon/internal/helpers" - "github.com/haileyok/cocoon/models" + "tangled.org/pdewey.com/chrysalis/internal/helpers" + "tangled.org/pdewey.com/chrysalis/models" "github.com/ipfs/go-cid" cbor "github.com/ipfs/go-ipld-cbor" "github.com/ipld/go-car" diff --git a/server/handle_sync_get_repo.go b/server/handle_sync_get_repo.go index 152f14a..1194fe0 100644 --- a/server/handle_sync_get_repo.go +++ b/server/handle_sync_get_repo.go @@ -4,8 +4,8 @@ import ( "bytes" "github.com/bluesky-social/indigo/carstore" - "github.com/haileyok/cocoon/internal/helpers" - "github.com/haileyok/cocoon/models" + "tangled.org/pdewey.com/chrysalis/internal/helpers" + "tangled.org/pdewey.com/chrysalis/models" "github.com/ipfs/go-cid" cbor "github.com/ipfs/go-ipld-cbor" "github.com/ipld/go-car" diff --git a/server/handle_sync_get_repo_status.go b/server/handle_sync_get_repo_status.go index 442f83e..8ca8da9 100644 --- a/server/handle_sync_get_repo_status.go +++ b/server/handle_sync_get_repo_status.go @@ -1,7 +1,7 @@ package server import ( - "github.com/haileyok/cocoon/internal/helpers" + "tangled.org/pdewey.com/chrysalis/internal/helpers" "github.com/labstack/echo/v4" ) diff --git a/server/handle_sync_list_blobs.go b/server/handle_sync_list_blobs.go index c3d2d49..ea174f4 100644 --- a/server/handle_sync_list_blobs.go +++ b/server/handle_sync_list_blobs.go @@ -1,8 +1,8 @@ package server import ( - "github.com/haileyok/cocoon/internal/helpers" - "github.com/haileyok/cocoon/models" + "tangled.org/pdewey.com/chrysalis/internal/helpers" + "tangled.org/pdewey.com/chrysalis/models" "github.com/ipfs/go-cid" "github.com/labstack/echo/v4" ) diff --git a/server/handle_sync_subscribe_repos.go b/server/handle_sync_subscribe_repos.go index 13ab44f..b6d758b 100644 --- a/server/handle_sync_subscribe_repos.go +++ b/server/handle_sync_subscribe_repos.go @@ -7,8 +7,9 @@ import ( "github.com/bluesky-social/indigo/events" "github.com/bluesky-social/indigo/lex/util" - "github.com/btcsuite/websocket" - "github.com/haileyok/cocoon/metrics" + "net/http" + + "github.com/gorilla/websocket" "github.com/labstack/echo/v4" ) @@ -18,7 +19,13 @@ func (s *Server) handleSyncSubscribeRepos(e echo.Context) error { logger := s.logger.With("component", "subscribe-repos-websocket") - conn, err := websocket.Upgrade(e.Response().Writer, e.Request(), e.Response().Header(), 1<<10, 1<<10) + upgrader := websocket.Upgrader{ + ReadBufferSize: 1 << 10, + WriteBufferSize: 1 << 10, + CheckOrigin: func(r *http.Request) bool { return true }, + } + + conn, err := upgrader.Upgrade(e.Response().Writer, e.Request(), nil) if err != nil { logger.Error("unable to establish websocket with relay", "err", err) return err @@ -39,11 +46,6 @@ func (s *Server) handleSyncSubscribeRepos(e echo.Context) error { } } - 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 }, since) @@ -71,10 +73,6 @@ func (s *Server) handleSyncSubscribeRepos(e echo.Context) error { 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) diff --git a/server/handle_well_known.go b/server/handle_well_known.go index 1f53655..09b8f09 100644 --- a/server/handle_well_known.go +++ b/server/handle_well_known.go @@ -4,7 +4,7 @@ import ( "fmt" "strings" -"github.com/haileyok/cocoon/internal/helpers" +"tangled.org/pdewey.com/chrysalis/internal/helpers" "github.com/labstack/echo/v4" "gorm.io/gorm" ) diff --git a/server/mail.go b/server/mail.go index 759625c..8218e46 100644 --- a/server/mail.go +++ b/server/mail.go @@ -1,117 +1,10 @@ package server -import "fmt" - -func (s *Server) sendWelcomeMail(email, handle string) error { - if s.mail == nil { - return nil - } - - s.mailLk.Lock() - defer s.mailLk.Unlock() - - s.mail.To(email) - s.mail.Subject("Welcome to " + s.config.Hostname) - s.mail.Plain().Set(fmt.Sprintf("Welcome to %s! Your handle is %s.", email, handle)) - - if err := s.mail.Send(); err != nil { - return err - } - - return nil -} - -func (s *Server) sendPasswordReset(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("Password reset for " + s.config.Hostname) - s.mail.Plain().Set(fmt.Sprintf("Hello %s. Your password reset code is %s. This code will expire in ten minutes.", handle, code)) - - if err := s.mail.Send(); err != nil { - return err - } - - return nil -} - -func (s *Server) sendPlcTokenReset(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("PLC token for " + s.config.Hostname) - s.mail.Plain().Set(fmt.Sprintf("Hello %s. Your PLC operation code is %s. This code will expire in ten minutes.", handle, code)) - - if err := s.mail.Send(); err != nil { - return err - } - - return nil -} - -func (s *Server) sendEmailUpdate(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("Email update for " + s.config.Hostname) - s.mail.Plain().Set(fmt.Sprintf("Hello %s. Your email update code is %s. This code will expire in ten minutes.", handle, code)) - - if err := s.mail.Send(); err != nil { - return err - } - - return nil -} - -func (s *Server) sendEmailVerification(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("Email verification for " + s.config.Hostname) - s.mail.Plain().Set(fmt.Sprintf("Hello %s. Your email verification code is %s. This code will expire in ten minutes.", handle, code)) - - if err := s.mail.Send(); err != nil { - return err - } - - 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 -} +// Mail stubs — chrysalis is a test-only PDS, no SMTP support. + +func (s *Server) sendWelcomeMail(email, handle string) error { return nil } +func (s *Server) sendPasswordReset(email, handle, code string) error { return nil } +func (s *Server) sendPlcTokenReset(email, handle, code string) error { return nil } +func (s *Server) sendEmailUpdate(email, handle, code string) error { return nil } +func (s *Server) sendEmailVerification(email, handle, code string) error { return nil } +func (s *Server) sendTwoFactorCode(email, handle, code string) error { return nil } diff --git a/server/middleware.go b/server/middleware.go index b4a5fa2..64cb47c 100644 --- a/server/middleware.go +++ b/server/middleware.go @@ -9,10 +9,10 @@ import ( "time" "github.com/golang-jwt/jwt/v4" - "github.com/haileyok/cocoon/internal/helpers" - "github.com/haileyok/cocoon/models" - "github.com/haileyok/cocoon/oauth/dpop" - "github.com/haileyok/cocoon/oauth/provider" + "tangled.org/pdewey.com/chrysalis/internal/helpers" + "tangled.org/pdewey.com/chrysalis/models" + "tangled.org/pdewey.com/chrysalis/oauth/dpop" + "tangled.org/pdewey.com/chrysalis/oauth/provider" "github.com/labstack/echo/v4" "gitlab.com/yawning/secp256k1-voi" secp256k1secec "gitlab.com/yawning/secp256k1-voi/secec" diff --git a/server/persist.go b/server/persist.go index 0ff510d..ba7ba7e 100644 --- a/server/persist.go +++ b/server/persist.go @@ -13,7 +13,7 @@ import ( cbg "github.com/whyrusleeping/cbor-gen" "gorm.io/gorm" - "github.com/haileyok/cocoon/models" + "tangled.org/pdewey.com/chrysalis/models" ) type DbPersister struct { diff --git a/server/repo.go b/server/repo.go index ebcef67..1587e77 100644 --- a/server/repo.go +++ b/server/repo.go @@ -18,10 +18,9 @@ import ( "github.com/bluesky-social/indigo/carstore" "github.com/bluesky-social/indigo/events" lexutil "github.com/bluesky-social/indigo/lex/util" - "github.com/haileyok/cocoon/internal/db" - "github.com/haileyok/cocoon/metrics" - "github.com/haileyok/cocoon/models" - "github.com/haileyok/cocoon/recording_blockstore" + "tangled.org/pdewey.com/chrysalis/internal/db" + "tangled.org/pdewey.com/chrysalis/models" + "tangled.org/pdewey.com/chrysalis/recording_blockstore" blocks "github.com/ipfs/go-block-format" "github.com/ipfs/go-cid" blockstore "github.com/ipfs/go-ipfs-blockstore" @@ -408,12 +407,6 @@ func (rm *RepoMan) applyWrites(ctx context.Context, urepo models.Repo, writes [] 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) diff --git a/server/server.go b/server/server.go index dc43679..9372980 100644 --- a/server/server.go +++ b/server/server.go @@ -1,93 +1,57 @@ package server import ( - "bytes" "context" "crypto/ecdsa" - "embed" "errors" "fmt" - "io" "log/slog" "net/http" - "net/smtp" "os" - "path/filepath" "sync" - "text/template" "time" - "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/aws/credentials" - "github.com/aws/aws-sdk-go/aws/session" - "github.com/aws/aws-sdk-go/service/s3" "github.com/bluesky-social/indigo/api/atproto" "github.com/bluesky-social/indigo/atproto/syntax" "github.com/bluesky-social/indigo/events" "github.com/bluesky-social/indigo/util" "github.com/bluesky-social/indigo/xrpc" - "github.com/domodwyer/mailyak/v3" "github.com/go-playground/validator" - "github.com/gorilla/sessions" - "github.com/haileyok/cocoon/identity" - "github.com/haileyok/cocoon/internal/db" - "github.com/haileyok/cocoon/internal/helpers" - "github.com/haileyok/cocoon/models" - "github.com/haileyok/cocoon/oauth/client" - "github.com/haileyok/cocoon/oauth/constants" - "github.com/haileyok/cocoon/oauth/dpop" - "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" - "github.com/prometheus/client_golang/prometheus" slogecho "github.com/samber/slog-echo" - "gorm.io/driver/postgres" "gorm.io/driver/sqlite" "gorm.io/gorm" + "tangled.org/pdewey.com/chrysalis/identity" + "tangled.org/pdewey.com/chrysalis/internal/db" + "tangled.org/pdewey.com/chrysalis/internal/helpers" + "tangled.org/pdewey.com/chrysalis/models" + "tangled.org/pdewey.com/chrysalis/oauth/client" + "tangled.org/pdewey.com/chrysalis/oauth/constants" + "tangled.org/pdewey.com/chrysalis/oauth/dpop" + "tangled.org/pdewey.com/chrysalis/oauth/provider" + "tangled.org/pdewey.com/chrysalis/plc" ) -const ( - AccountSessionMaxAge = 30 * 24 * time.Hour // one week -) - -type S3Config struct { - BackupsEnabled bool - BlobstoreEnabled bool - Endpoint string - Region string - Bucket string - AccessKey string - SecretKey string - CDNUrl string -} - type Server struct { - http *http.Client - httpd *http.Server - mail *mailyak.MailYak - mailLk *sync.Mutex - echo *echo.Echo - db *db.DB - plcClient plc.PLCClient - logger *slog.Logger - config *config - privateKey *ecdsa.PrivateKey - repoman *RepoMan + http *http.Client + httpd *http.Server + echo *echo.Echo + db *db.DB + plcClient plc.PLCClient + logger *slog.Logger + config *config + privateKey *ecdsa.PrivateKey + repoman *RepoMan oauthProvider *provider.Provider - evtman *events.EventManager - passport *identity.Passport - fallbackProxy string + evtman *events.EventManager + passport *identity.Passport lastRequestCrawl time.Time requestCrawlMu sync.Mutex - dbName string - dbType string - s3Config *S3Config + dbName string } type Args struct { @@ -96,9 +60,6 @@ type Args struct { LogLevel slog.Level Addr string DbName string - DbType string - DatabaseURL string - Version string Did string Hostname string RotationKeyPath string @@ -108,46 +69,26 @@ type Args struct { AdminPassword string RequireInvite bool - SmtpUser string - SmtpPass string - SmtpHost string - SmtpPort string - SmtpEmail string - SmtpName string - - S3Config *S3Config - - SessionSecret string - SessionCookieKey string - BlockstoreVariant BlockstoreVariant - FallbackProxy string // Embedding overrides: point at a custom PLC or swap the outbound // HTTP client. All optional; fall back to defaults when unset. PLCUrl string // defaults to https://plc.directory HTTPClient *http.Client // defaults to util.RobustHTTPClient() - // Test / multi-instance overrides. Production should leave these nil. - PLCClient plc.PLCClient // replaces the real PLC client entirely; PLCUrl is ignored - PrometheusRegisterer prometheus.Registerer // per-instance registry to avoid global collisions + // Test / multi-instance overrides. + PLCClient plc.PLCClient // replaces the real PLC client entirely; PLCUrl is ignored } type config struct { LogLevel slog.Level - Version string Did string Hostname string ContactEmail string - EnforcePeering bool Relays []string AdminPassword string RequireInvite bool - SmtpEmail string - SmtpName string - SessionCookieKey string BlockstoreVariant BlockstoreVariant - FallbackProxy string } type CustomValidator struct { @@ -178,52 +119,6 @@ func (cv *CustomValidator) Validate(i any) error { return nil } -//go:embed templates/* -var templateFS embed.FS - -//go:embed static/* -var staticFS embed.FS - -type TemplateRenderer struct { - templates *template.Template - isDev bool - templatePath string -} - -func (s *Server) loadTemplates() { - absPath, _ := filepath.Abs("server/templates/*.html") - if s.config.Version == "dev" { - tmpl := template.Must(template.ParseGlob(absPath)) - s.echo.Renderer = &TemplateRenderer{ - templates: tmpl, - isDev: true, - templatePath: absPath, - } - } else { - tmpl := template.Must(template.ParseFS(templateFS, "templates/*.html")) - s.echo.Renderer = &TemplateRenderer{ - templates: tmpl, - isDev: false, - } - } -} - -func (t *TemplateRenderer) Render(w io.Writer, name string, data any, c echo.Context) error { - if t.isDev { - tmpl, err := template.ParseGlob(t.templatePath) - if err != nil { - return err - } - t.templates = tmpl - } - - if viewContext, isMap := data.(map[string]any); isMap { - viewContext["reverse"] = c.Echo().Reverse - } - - return t.templates.ExecuteTemplate(w, name, data) -} - type filteredHandler struct { level slog.Level handler slog.Handler @@ -268,42 +163,29 @@ func New(args *Args) (*Server, error) { } if args.Did == "" { - return nil, fmt.Errorf("cocoon did must be set") + return nil, fmt.Errorf("did must be set") } if args.ContactEmail == "" { - return nil, fmt.Errorf("cocoon contact email is required") + return nil, fmt.Errorf("contact email is required") } if _, err := syntax.ParseDID(args.Did); err != nil { - return nil, fmt.Errorf("error parsing cocoon did: %w", err) + return nil, fmt.Errorf("error parsing did: %w", err) } if args.Hostname == "" { - return nil, fmt.Errorf("cocoon hostname must be set") + return nil, fmt.Errorf("hostname must be set") } if args.AdminPassword == "" { 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)))) - if args.PrometheusRegisterer != nil { - e.Use(echoprometheus.NewMiddlewareWithConfig(echoprometheus.MiddlewareConfig{ - Subsystem: "cocoon", - Registerer: args.PrometheusRegisterer, - })) - } else { - e.Use(echoprometheus.NewMiddleware("cocoon")) - } e.Use(middleware.CORSWithConfig(middleware.CORSConfig{ AllowOrigins: []string{"*"}, AllowHeaders: []string{"*"}, @@ -349,33 +231,14 @@ func New(args *Args) (*Server, error) { IdleTimeout: 5 * time.Minute, } - dbType := args.DbType - if dbType == "" { - dbType = "sqlite" + gdb, err := gorm.Open(sqlite.Open(args.DbName), &gorm.Config{}) + if err != nil { + return nil, fmt.Errorf("failed to open sqlite database: %w", err) } + gdb.Exec("PRAGMA journal_mode=WAL") + gdb.Exec("PRAGMA synchronous=NORMAL") + logger.Info("connected to SQLite database", "path", args.DbName) - var gdb *gorm.DB - var err error - switch dbType { - case "postgres": - if args.DatabaseURL == "" { - return nil, fmt.Errorf("database-url must be set when using postgres") - } - gdb, err = gorm.Open(postgres.Open(args.DatabaseURL), &gorm.Config{}) - 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) - } - gdb.Exec("PRAGMA journal_mode=WAL") - gdb.Exec("PRAGMA synchronous=NORMAL") - - logger.Info("connected to SQLite database", "path", args.DbName) - } dbw := db.NewDB(gdb) rkbytes, err := os.ReadFile(args.RotationKeyPath) @@ -445,26 +308,18 @@ func New(args *Args) (*Server, error) { privateKey: &pkey, config: &config{ LogLevel: args.LogLevel, - Version: args.Version, Did: args.Did, Hostname: args.Hostname, ContactEmail: args.ContactEmail, - EnforcePeering: false, Relays: args.Relays, AdminPassword: args.AdminPassword, RequireInvite: args.RequireInvite, - SmtpName: args.SmtpName, - SmtpEmail: args.SmtpEmail, - SessionCookieKey: args.SessionCookieKey, BlockstoreVariant: args.BlockstoreVariant, - FallbackProxy: args.FallbackProxy, }, evtman: events.NewEventManager(evtPersister), passport: identity.NewPassport(h, identity.NewMemCache(10_000)), - dbName: args.DbName, - dbType: dbType, - s3Config: args.S3Config, + dbName: args.DbName, oauthProvider: provider.NewProvider(provider.Args{ Hostname: args.Hostname, @@ -486,35 +341,12 @@ func New(args *Args) (*Server, error) { }), } - s.loadTemplates() - - s.repoman = NewRepoMan(s) // TODO: this is way too lazy, stop it - - // TODO: should validate these args - if args.SmtpUser == "" || args.SmtpPass == "" || args.SmtpHost == "" || args.SmtpPort == "" || args.SmtpEmail == "" || args.SmtpName == "" { - args.Logger.Warn("not enough smtp args were provided. mailing will not work for your server.") - } else { - mail := mailyak.New(args.SmtpHost+":"+args.SmtpPort, smtp.PlainAuth("", args.SmtpUser, args.SmtpPass, args.SmtpHost)) - mail.From(s.config.SmtpEmail) - mail.FromName(s.config.SmtpName) - - s.mail = mail - s.mailLk = &sync.Mutex{} - } + s.repoman = NewRepoMan(s) return s, nil } func (s *Server) addRoutes() { - // static - if s.config.Version == "dev" { - s.echo.Static("/static", "server/static") - } else { - s.echo.GET("/static/*", echo.WrapHandler(http.FileServer(http.FS(staticFS)))) - } - - // random stuff - s.echo.GET("/", s.handleRoot) s.echo.GET("/xrpc/_health", s.handleHealth) s.echo.GET("/.well-known/did.json", s.handleWellKnown) s.echo.GET("/.well-known/atproto-did", s.handleAtprotoDid) @@ -545,17 +377,8 @@ func (s *Server) addRoutes() { // labels s.echo.GET("/xrpc/com.atproto.label.queryLabels", s.handleLabelQueryLabels) - // account - s.echo.GET("/account", s.handleAccount) - s.echo.POST("/account/revoke", s.handleAccountRevoke) - s.echo.GET("/account/signin", s.handleAccountSigninGet) - s.echo.POST("/account/signin", s.handleAccountSigninPost) - s.echo.GET("/account/signout", s.handleAccountSignout) - - // oauth account + // oauth s.echo.GET("/oauth/jwks", s.handleOauthJwks) - s.echo.GET("/oauth/authorize", s.handleOauthAuthorizeGet) - s.echo.POST("/oauth/authorize", s.handleOauthAuthorizePost) // oauth authorization s.echo.POST("/oauth/par", s.handleOauthPar, s.oauthProvider.BaseMiddleware) @@ -572,7 +395,7 @@ func (s *Server) addRoutes() { s.echo.POST("/xrpc/com.atproto.identity.submitPlcOperation", s.handleSubmitPlcOperation, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware) s.echo.POST("/xrpc/com.atproto.server.confirmEmail", s.handleServerConfirmEmail, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware) s.echo.POST("/xrpc/com.atproto.server.requestEmailConfirmation", s.handleServerRequestEmailConfirmation, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware) - s.echo.POST("/xrpc/com.atproto.server.requestPasswordReset", s.handleServerRequestPasswordReset) // AUTH NOT REQUIRED FOR THIS ONE + s.echo.POST("/xrpc/com.atproto.server.requestPasswordReset", s.handleServerRequestPasswordReset) s.echo.POST("/xrpc/com.atproto.server.requestEmailUpdate", s.handleServerRequestEmailUpdate, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware) s.echo.POST("/xrpc/com.atproto.server.resetPassword", s.handleServerResetPassword, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware) s.echo.POST("/xrpc/com.atproto.server.updateEmail", s.handleServerUpdateEmail, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware) @@ -592,18 +415,14 @@ func (s *Server) addRoutes() { s.echo.POST("/xrpc/com.atproto.repo.uploadBlob", s.handleRepoUploadBlob, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware) s.echo.POST("/xrpc/com.atproto.repo.importRepo", s.handleRepoImportRepo, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware) - // stupid silly endpoints + // bsky compat s.echo.GET("/xrpc/app.bsky.actor.getPreferences", s.handleActorGetPreferences, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware) s.echo.POST("/xrpc/app.bsky.actor.putPreferences", s.handleActorPutPreferences, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware) - s.echo.GET("/xrpc/app.bsky.feed.getFeed", s.handleProxyBskyFeedGetFeed, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware) s.echo.GET("/xrpc/app.bsky.ageassurance.getState", s.handleAgeAssurance, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware) + // admin routes s.echo.POST("/xrpc/com.atproto.server.createInviteCode", s.handleCreateInviteCode, s.handleAdminMiddleware) s.echo.POST("/xrpc/com.atproto.server.createInviteCodes", s.handleCreateInviteCodes, s.handleAdminMiddleware) - - // are there any routes that we should be allowing without auth? i dont think so but idk - s.echo.GET("/xrpc/*", s.handleProxy, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware) - s.echo.POST("/xrpc/*", s.handleProxy, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware) } func (s *Server) Serve(ctx context.Context) error { @@ -628,7 +447,7 @@ func (s *Server) Serve(ctx context.Context) error { &provider.OauthAuthorizationRequest{}, ) - logger.Info("starting cocoon") + logger.Info("starting chrysalis") listenErr := make(chan error, 1) go func() { @@ -637,8 +456,6 @@ func (s *Server) Serve(ctx context.Context) error { } }() - go s.backupRoutine() - go func() { if err := s.requestCrawl(ctx); err != nil { logger.Error("error requesting crawls", "err", err) @@ -692,119 +509,6 @@ func (s *Server) requestCrawl(ctx context.Context) error { return nil } -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...") - - tmpFile := fmt.Sprintf("/tmp/cocoon-backup-%s.db", time.Now().Format(time.RFC3339Nano)) - defer os.Remove(tmpFile) - - if err := s.db.Client().Exec(fmt.Sprintf("VACUUM INTO '%s'", tmpFile)).Error; err != nil { - logger.Error("error creating tmp backup file", "err", err) - return - } - - backupData, err := os.ReadFile(tmpFile) - if err != nil { - logger.Error("error reading tmp backup file", "err", err) - return - } - - logger.Info("sending to s3...") - - currTime := time.Now().Format("2006-01-02_15-04-05") - key := "cocoon-backup-" + currTime + ".db" - - config := &aws.Config{ - Region: new(s.s3Config.Region), - Credentials: credentials.NewStaticCredentials(s.s3Config.AccessKey, s.s3Config.SecretKey, ""), - } - - if s.s3Config.Endpoint != "" { - config.Endpoint = new(s.s3Config.Endpoint) - config.S3ForcePathStyle = new(true) - } - - sess, err := session.NewSession(config) - if err != nil { - logger.Error("error creating s3 session", "err", err) - return - } - - svc := s3.New(sess) - - if _, err := svc.PutObject(&s3.PutObjectInput{ - Bucket: new(s.s3Config.Bucket), - Key: new(key), - Body: bytes.NewReader(backupData), - }); err != nil { - logger.Error("error uploading file to s3", "err", err) - return - } - - logger.Info("finished uploading backup to s3", "key", key, "duration", time.Since(start).Seconds()) - - os.WriteFile("last-backup.txt", []byte(time.Now().Format(time.RFC3339Nano)), 0644) -} - -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 - } - - shouldBackupNow := false - lastBackupStr, err := os.ReadFile("last-backup.txt") - if err != nil { - shouldBackupNow = true - } else { - lastBackup, err := time.Parse(time.RFC3339Nano, string(lastBackupStr)) - if err != nil { - shouldBackupNow = true - } else if time.Since(lastBackup).Seconds() > 3600 { - shouldBackupNow = true - } - } - - if shouldBackupNow { - go s.doBackup() - } - - ticker := time.NewTicker(time.Hour) - for range ticker.C { - go s.doBackup() - } -} - 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 diff --git a/server/session.go b/server/session.go index 5db2677..6d1c46a 100644 --- a/server/session.go +++ b/server/session.go @@ -6,7 +6,7 @@ import ( "github.com/golang-jwt/jwt/v4" "github.com/google/uuid" - "github.com/haileyok/cocoon/models" + "tangled.org/pdewey.com/chrysalis/models" ) type Session struct { diff --git a/server/static/pico.css b/server/static/pico.css deleted file mode 100644 index cbbe2c3..0000000 --- a/server/static/pico.css +++ /dev/null @@ -1,4 +0,0 @@ -@charset "UTF-8";/*! - * Pico CSS ✨ v2.1.1 (https://picocss.com) - * Copyright 2019-2025 - Licensed under MIT - */:host,:root{--pico-font-family-emoji:"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--pico-font-family-sans-serif:system-ui,"Segoe UI",Roboto,Oxygen,Ubuntu,Cantarell,Helvetica,Arial,"Helvetica Neue",sans-serif,var(--pico-font-family-emoji);--pico-font-family-monospace:ui-monospace,SFMono-Regular,"SF Mono",Menlo,Consolas,"Liberation Mono",monospace,var(--pico-font-family-emoji);--pico-font-family:var(--pico-font-family-sans-serif);--pico-line-height:1.5;--pico-font-weight:400;--pico-font-size:100%;--pico-text-underline-offset:0.1rem;--pico-border-radius:0.25rem;--pico-border-width:0.0625rem;--pico-outline-width:0.125rem;--pico-transition:0.2s ease-in-out;--pico-spacing:1rem;--pico-typography-spacing-vertical:1rem;--pico-block-spacing-vertical:var(--pico-spacing);--pico-block-spacing-horizontal:var(--pico-spacing);--pico-grid-column-gap:var(--pico-spacing);--pico-grid-row-gap:var(--pico-spacing);--pico-form-element-spacing-vertical:0.75rem;--pico-form-element-spacing-horizontal:1rem;--pico-group-box-shadow:0 0 0 rgba(0, 0, 0, 0);--pico-group-box-shadow-focus-with-button:0 0 0 var(--pico-outline-width) var(--pico-primary-focus);--pico-group-box-shadow-focus-with-input:0 0 0 0.0625rem var(--pico-form-element-border-color);--pico-modal-overlay-backdrop-filter:blur(0.375rem);--pico-nav-element-spacing-vertical:1rem;--pico-nav-element-spacing-horizontal:0.5rem;--pico-nav-link-spacing-vertical:0.5rem;--pico-nav-link-spacing-horizontal:0.5rem;--pico-nav-breadcrumb-divider:">";--pico-icon-checkbox:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(255, 255, 255)' stroke-width='4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'%3E%3C/polyline%3E%3C/svg%3E");--pico-icon-minus:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(255, 255, 255)' stroke-width='4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cline x1='5' y1='12' x2='19' y2='12'%3E%3C/line%3E%3C/svg%3E");--pico-icon-chevron:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(136, 145, 164)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");--pico-icon-date:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(136, 145, 164)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3' y='4' width='18' height='18' rx='2' ry='2'%3E%3C/rect%3E%3Cline x1='16' y1='2' x2='16' y2='6'%3E%3C/line%3E%3Cline x1='8' y1='2' x2='8' y2='6'%3E%3C/line%3E%3Cline x1='3' y1='10' x2='21' y2='10'%3E%3C/line%3E%3C/svg%3E");--pico-icon-time:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(136, 145, 164)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cpolyline points='12 6 12 12 16 14'%3E%3C/polyline%3E%3C/svg%3E");--pico-icon-search:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(136, 145, 164)' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='11' cy='11' r='8'%3E%3C/circle%3E%3Cline x1='21' y1='21' x2='16.65' y2='16.65'%3E%3C/line%3E%3C/svg%3E");--pico-icon-close:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(136, 145, 164)' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cline x1='18' y1='6' x2='6' y2='18'%3E%3C/line%3E%3Cline x1='6' y1='6' x2='18' y2='18'%3E%3C/line%3E%3C/svg%3E");--pico-icon-loading:url("data:image/svg+xml,%3Csvg fill='none' height='24' width='24' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg' %3E%3Cstyle%3E g %7B animation: rotate 2s linear infinite; transform-origin: center center; %7D circle %7B stroke-dasharray: 75,100; stroke-dashoffset: -5; animation: dash 1.5s ease-in-out infinite; stroke-linecap: round; %7D @keyframes rotate %7B 0%25 %7B transform: rotate(0deg); %7D 100%25 %7B transform: rotate(360deg); %7D %7D @keyframes dash %7B 0%25 %7B stroke-dasharray: 1,100; stroke-dashoffset: 0; %7D 50%25 %7B stroke-dasharray: 44.5,100; stroke-dashoffset: -17.5; %7D 100%25 %7B stroke-dasharray: 44.5,100; stroke-dashoffset: -62; %7D %7D %3C/style%3E%3Cg%3E%3Ccircle cx='12' cy='12' r='10' fill='none' stroke='rgb(136, 145, 164)' stroke-width='4' /%3E%3C/g%3E%3C/svg%3E")}@media (min-width:576px){:host,:root{--pico-font-size:106.25%}}@media (min-width:768px){:host,:root{--pico-font-size:112.5%}}@media (min-width:1024px){:host,:root{--pico-font-size:118.75%}}@media (min-width:1280px){:host,:root{--pico-font-size:125%}}@media (min-width:1536px){:host,:root{--pico-font-size:131.25%}}a{--pico-text-decoration:underline}a.contrast,a.secondary{--pico-text-decoration:underline}small{--pico-font-size:0.875em}h1,h2,h3,h4,h5,h6{--pico-font-weight:700}h1{--pico-font-size:2rem;--pico-line-height:1.125;--pico-typography-spacing-top:3rem}h2{--pico-font-size:1.75rem;--pico-line-height:1.15;--pico-typography-spacing-top:2.625rem}h3{--pico-font-size:1.5rem;--pico-line-height:1.175;--pico-typography-spacing-top:2.25rem}h4{--pico-font-size:1.25rem;--pico-line-height:1.2;--pico-typography-spacing-top:1.874rem}h5{--pico-font-size:1.125rem;--pico-line-height:1.225;--pico-typography-spacing-top:1.6875rem}h6{--pico-font-size:1rem;--pico-line-height:1.25;--pico-typography-spacing-top:1.5rem}tfoot td,tfoot th,thead td,thead th{--pico-font-weight:600;--pico-border-width:0.1875rem}code,kbd,pre,samp{--pico-font-family:var(--pico-font-family-monospace)}kbd{--pico-font-weight:bolder}:where(select,textarea),input:not([type=submit],[type=button],[type=reset],[type=checkbox],[type=radio],[type=file]){--pico-outline-width:0.0625rem}[type=search]{--pico-border-radius:5rem}[type=checkbox],[type=radio]{--pico-border-width:0.125rem}[type=checkbox][role=switch]{--pico-border-width:0.1875rem}details.dropdown summary:not([role=button]){--pico-outline-width:0.0625rem}nav details.dropdown summary:focus-visible{--pico-outline-width:0.125rem}[role=search]{--pico-border-radius:5rem}[role=group]:has(button.secondary:focus,[type=submit].secondary:focus,[type=button].secondary:focus,[role=button].secondary:focus),[role=search]:has(button.secondary:focus,[type=submit].secondary:focus,[type=button].secondary:focus,[role=button].secondary:focus){--pico-group-box-shadow-focus-with-button:0 0 0 var(--pico-outline-width) var(--pico-secondary-focus)}[role=group]:has(button.contrast:focus,[type=submit].contrast:focus,[type=button].contrast:focus,[role=button].contrast:focus),[role=search]:has(button.contrast:focus,[type=submit].contrast:focus,[type=button].contrast:focus,[role=button].contrast:focus){--pico-group-box-shadow-focus-with-button:0 0 0 var(--pico-outline-width) var(--pico-contrast-focus)}[role=group] [role=button],[role=group] [type=button],[role=group] [type=submit],[role=group] button,[role=search] [role=button],[role=search] [type=button],[role=search] [type=submit],[role=search] button{--pico-form-element-spacing-horizontal:2rem}details summary[role=button]:not(.outline)::after{filter:brightness(0) invert(1)}[aria-busy=true]:not(input,select,textarea):is(button,[type=submit],[type=button],[type=reset],[role=button]):not(.outline)::before{filter:brightness(0) invert(1)}:host(:not([data-theme=dark])),:root:not([data-theme=dark]),[data-theme=light]{color-scheme:light;--pico-background-color:#fff;--pico-color:#373c44;--pico-text-selection-color:rgba(116, 139, 248, 0.25);--pico-muted-color:#646b79;--pico-muted-border-color:rgb(231, 234, 239.5);--pico-primary:#2060df;--pico-primary-background:#2060df;--pico-primary-border:var(--pico-primary-background);--pico-primary-underline:rgba(32, 96, 223, 0.5);--pico-primary-hover:#184eb8;--pico-primary-hover-background:#1d59d0;--pico-primary-hover-border:var(--pico-primary-hover-background);--pico-primary-hover-underline:var(--pico-primary-hover);--pico-primary-focus:rgba(116, 139, 248, 0.5);--pico-primary-inverse:#fff;--pico-secondary:#5d6b89;--pico-secondary-background:#525f7a;--pico-secondary-border:var(--pico-secondary-background);--pico-secondary-underline:rgba(93, 107, 137, 0.5);--pico-secondary-hover:#48536b;--pico-secondary-hover-background:#48536b;--pico-secondary-hover-border:var(--pico-secondary-hover-background);--pico-secondary-hover-underline:var(--pico-secondary-hover);--pico-secondary-focus:rgba(93, 107, 137, 0.25);--pico-secondary-inverse:#fff;--pico-contrast:#181c25;--pico-contrast-background:#181c25;--pico-contrast-border:var(--pico-contrast-background);--pico-contrast-underline:rgba(24, 28, 37, 0.5);--pico-contrast-hover:#000;--pico-contrast-hover-background:#000;--pico-contrast-hover-border:var(--pico-contrast-hover-background);--pico-contrast-hover-underline:var(--pico-secondary-hover);--pico-contrast-focus:rgba(93, 107, 137, 0.25);--pico-contrast-inverse:#fff;--pico-box-shadow:0.0145rem 0.029rem 0.174rem rgba(129, 145, 181, 0.01698),0.0335rem 0.067rem 0.402rem rgba(129, 145, 181, 0.024),0.0625rem 0.125rem 0.75rem rgba(129, 145, 181, 0.03),0.1125rem 0.225rem 1.35rem rgba(129, 145, 181, 0.036),0.2085rem 0.417rem 2.502rem rgba(129, 145, 181, 0.04302),0.5rem 1rem 6rem rgba(129, 145, 181, 0.06),0 0 0 0.0625rem rgba(129, 145, 181, 0.015);--pico-h1-color:#2d3138;--pico-h2-color:#373c44;--pico-h3-color:#424751;--pico-h4-color:#4d535e;--pico-h5-color:#5c6370;--pico-h6-color:#646b79;--pico-mark-background-color:rgb(252.5, 230.5, 191.5);--pico-mark-color:#0f1114;--pico-ins-color:rgb(28.5, 105.5, 84);--pico-del-color:rgb(136, 56.5, 53);--pico-blockquote-border-color:var(--pico-muted-border-color);--pico-blockquote-footer-color:var(--pico-muted-color);--pico-button-box-shadow:0 0 0 rgba(0, 0, 0, 0);--pico-button-hover-box-shadow:0 0 0 rgba(0, 0, 0, 0);--pico-table-border-color:var(--pico-muted-border-color);--pico-table-row-stripped-background-color:rgba(111, 120, 135, 0.0375);--pico-code-background-color:rgb(243, 244.5, 246.75);--pico-code-color:#646b79;--pico-code-kbd-background-color:var(--pico-color);--pico-code-kbd-color:var(--pico-background-color);--pico-form-element-background-color:rgb(251, 251.5, 252.25);--pico-form-element-selected-background-color:#dfe3eb;--pico-form-element-border-color:#cfd5e2;--pico-form-element-color:#23262c;--pico-form-element-placeholder-color:var(--pico-muted-color);--pico-form-element-active-background-color:#fff;--pico-form-element-active-border-color:var(--pico-primary-border);--pico-form-element-focus-color:var(--pico-primary-border);--pico-form-element-disabled-opacity:0.5;--pico-form-element-invalid-border-color:rgb(183.5, 105.5, 106.5);--pico-form-element-invalid-active-border-color:rgb(200.25, 79.25, 72.25);--pico-form-element-invalid-focus-color:var(--pico-form-element-invalid-active-border-color);--pico-form-element-valid-border-color:rgb(76, 154.5, 137.5);--pico-form-element-valid-active-border-color:rgb(39, 152.75, 118.75);--pico-form-element-valid-focus-color:var(--pico-form-element-valid-active-border-color);--pico-switch-background-color:#bfc7d9;--pico-switch-checked-background-color:var(--pico-primary-background);--pico-switch-color:#fff;--pico-switch-thumb-box-shadow:0 0 0 rgba(0, 0, 0, 0);--pico-range-border-color:#dfe3eb;--pico-range-active-border-color:#bfc7d9;--pico-range-thumb-border-color:var(--pico-background-color);--pico-range-thumb-color:var(--pico-secondary-background);--pico-range-thumb-active-color:var(--pico-primary-background);--pico-accordion-border-color:var(--pico-muted-border-color);--pico-accordion-active-summary-color:var(--pico-primary-hover);--pico-accordion-close-summary-color:var(--pico-color);--pico-accordion-open-summary-color:var(--pico-muted-color);--pico-card-background-color:var(--pico-background-color);--pico-card-border-color:var(--pico-muted-border-color);--pico-card-box-shadow:var(--pico-box-shadow);--pico-card-sectioning-background-color:rgb(251, 251.5, 252.25);--pico-dropdown-background-color:#fff;--pico-dropdown-border-color:#eff1f4;--pico-dropdown-box-shadow:var(--pico-box-shadow);--pico-dropdown-color:var(--pico-color);--pico-dropdown-hover-background-color:#eff1f4;--pico-loading-spinner-opacity:0.5;--pico-modal-overlay-background-color:rgba(232, 234, 237, 0.75);--pico-progress-background-color:#dfe3eb;--pico-progress-color:var(--pico-primary-background);--pico-tooltip-background-color:var(--pico-contrast-background);--pico-tooltip-color:var(--pico-contrast-inverse);--pico-icon-valid:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(76, 154.5, 137.5)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'%3E%3C/polyline%3E%3C/svg%3E");--pico-icon-invalid:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(200.25, 79.25, 72.25)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cline x1='12' y1='8' x2='12' y2='12'%3E%3C/line%3E%3Cline x1='12' y1='16' x2='12.01' y2='16'%3E%3C/line%3E%3C/svg%3E")}:host(:not([data-theme=dark])) input:is([type=submit],[type=button],[type=reset],[type=checkbox],[type=radio],[type=file]),:root:not([data-theme=dark]) input:is([type=submit],[type=button],[type=reset],[type=checkbox],[type=radio],[type=file]),[data-theme=light] input:is([type=submit],[type=button],[type=reset],[type=checkbox],[type=radio],[type=file]){--pico-form-element-focus-color:var(--pico-primary-focus)}@media only screen and (prefers-color-scheme:dark){:host(:not([data-theme])),:root:not([data-theme]){color-scheme:dark;--pico-background-color:rgb(19, 22.5, 30.5);--pico-color:#c2c7d0;--pico-text-selection-color:rgba(137, 153, 249, 0.1875);--pico-muted-color:#7b8495;--pico-muted-border-color:#202632;--pico-primary:#8999f9;--pico-primary-background:#2060df;--pico-primary-border:var(--pico-primary-background);--pico-primary-underline:rgba(137, 153, 249, 0.5);--pico-primary-hover:#aeb5fb;--pico-primary-hover-background:#3c71f7;--pico-primary-hover-border:var(--pico-primary-hover-background);--pico-primary-hover-underline:var(--pico-primary-hover);--pico-primary-focus:rgba(137, 153, 249, 0.375);--pico-primary-inverse:#fff;--pico-secondary:#969eaf;--pico-secondary-background:#525f7a;--pico-secondary-border:var(--pico-secondary-background);--pico-secondary-underline:rgba(150, 158, 175, 0.5);--pico-secondary-hover:#b3b9c5;--pico-secondary-hover-background:#5d6b89;--pico-secondary-hover-border:var(--pico-secondary-hover-background);--pico-secondary-hover-underline:var(--pico-secondary-hover);--pico-secondary-focus:rgba(144, 158, 190, 0.25);--pico-secondary-inverse:#fff;--pico-contrast:#dfe3eb;--pico-contrast-background:#eff1f4;--pico-contrast-border:var(--pico-contrast-background);--pico-contrast-underline:rgba(223, 227, 235, 0.5);--pico-contrast-hover:#fff;--pico-contrast-hover-background:#fff;--pico-contrast-hover-border:var(--pico-contrast-hover-background);--pico-contrast-hover-underline:var(--pico-contrast-hover);--pico-contrast-focus:rgba(207, 213, 226, 0.25);--pico-contrast-inverse:#000;--pico-box-shadow:0.0145rem 0.029rem 0.174rem rgba(7, 8.5, 12, 0.01698),0.0335rem 0.067rem 0.402rem rgba(7, 8.5, 12, 0.024),0.0625rem 0.125rem 0.75rem rgba(7, 8.5, 12, 0.03),0.1125rem 0.225rem 1.35rem rgba(7, 8.5, 12, 0.036),0.2085rem 0.417rem 2.502rem rgba(7, 8.5, 12, 0.04302),0.5rem 1rem 6rem rgba(7, 8.5, 12, 0.06),0 0 0 0.0625rem rgba(7, 8.5, 12, 0.015);--pico-h1-color:#f0f1f3;--pico-h2-color:#e0e3e7;--pico-h3-color:#c2c7d0;--pico-h4-color:#b3b9c5;--pico-h5-color:#a4acba;--pico-h6-color:#8891a4;--pico-mark-background-color:#014063;--pico-mark-color:#fff;--pico-ins-color:#62af9a;--pico-del-color:rgb(205.5, 126, 123);--pico-blockquote-border-color:var(--pico-muted-border-color);--pico-blockquote-footer-color:var(--pico-muted-color);--pico-button-box-shadow:0 0 0 rgba(0, 0, 0, 0);--pico-button-hover-box-shadow:0 0 0 rgba(0, 0, 0, 0);--pico-table-border-color:var(--pico-muted-border-color);--pico-table-row-stripped-background-color:rgba(111, 120, 135, 0.0375);--pico-code-background-color:rgb(26, 30.5, 40.25);--pico-code-color:#8891a4;--pico-code-kbd-background-color:var(--pico-color);--pico-code-kbd-color:var(--pico-background-color);--pico-form-element-background-color:rgb(28, 33, 43.5);--pico-form-element-selected-background-color:#2a3140;--pico-form-element-border-color:#2a3140;--pico-form-element-color:#e0e3e7;--pico-form-element-placeholder-color:#8891a4;--pico-form-element-active-background-color:rgb(26, 30.5, 40.25);--pico-form-element-active-border-color:var(--pico-primary-border);--pico-form-element-focus-color:var(--pico-primary-border);--pico-form-element-disabled-opacity:0.5;--pico-form-element-invalid-border-color:rgb(149.5, 74, 80);--pico-form-element-invalid-active-border-color:rgb(183.25, 63.5, 59);--pico-form-element-invalid-focus-color:var(--pico-form-element-invalid-active-border-color);--pico-form-element-valid-border-color:#2a7b6f;--pico-form-element-valid-active-border-color:rgb(22, 137, 105.5);--pico-form-element-valid-focus-color:var(--pico-form-element-valid-active-border-color);--pico-switch-background-color:#333c4e;--pico-switch-checked-background-color:var(--pico-primary-background);--pico-switch-color:#fff;--pico-switch-thumb-box-shadow:0 0 0 rgba(0, 0, 0, 0);--pico-range-border-color:#202632;--pico-range-active-border-color:#2a3140;--pico-range-thumb-border-color:var(--pico-background-color);--pico-range-thumb-color:var(--pico-secondary-background);--pico-range-thumb-active-color:var(--pico-primary-background);--pico-accordion-border-color:var(--pico-muted-border-color);--pico-accordion-active-summary-color:var(--pico-primary-hover);--pico-accordion-close-summary-color:var(--pico-color);--pico-accordion-open-summary-color:var(--pico-muted-color);--pico-card-background-color:#181c25;--pico-card-border-color:var(--pico-card-background-color);--pico-card-box-shadow:var(--pico-box-shadow);--pico-card-sectioning-background-color:rgb(26, 30.5, 40.25);--pico-dropdown-background-color:#181c25;--pico-dropdown-border-color:#202632;--pico-dropdown-box-shadow:var(--pico-box-shadow);--pico-dropdown-color:var(--pico-color);--pico-dropdown-hover-background-color:#202632;--pico-loading-spinner-opacity:0.5;--pico-modal-overlay-background-color:rgba(7.5, 8.5, 10, 0.75);--pico-progress-background-color:#202632;--pico-progress-color:var(--pico-primary-background);--pico-tooltip-background-color:var(--pico-contrast-background);--pico-tooltip-color:var(--pico-contrast-inverse);--pico-icon-valid:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(42, 123, 111)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'%3E%3C/polyline%3E%3C/svg%3E");--pico-icon-invalid:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(149.5, 74, 80)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cline x1='12' y1='8' x2='12' y2='12'%3E%3C/line%3E%3Cline x1='12' y1='16' x2='12.01' y2='16'%3E%3C/line%3E%3C/svg%3E")}:host(:not([data-theme])) input:is([type=submit],[type=button],[type=reset],[type=checkbox],[type=radio],[type=file]),:root:not([data-theme]) input:is([type=submit],[type=button],[type=reset],[type=checkbox],[type=radio],[type=file]){--pico-form-element-focus-color:var(--pico-primary-focus)}:host(:not([data-theme])) details summary[role=button].contrast:not(.outline)::after,:root:not([data-theme]) details summary[role=button].contrast:not(.outline)::after{filter:brightness(0)}:host(:not([data-theme])) [aria-busy=true]:not(input,select,textarea).contrast:is(button,[type=submit],[type=button],[type=reset],[role=button]):not(.outline)::before,:root:not([data-theme]) [aria-busy=true]:not(input,select,textarea).contrast:is(button,[type=submit],[type=button],[type=reset],[role=button]):not(.outline)::before{filter:brightness(0)}}[data-theme=dark]{color-scheme:dark;--pico-background-color:rgb(19, 22.5, 30.5);--pico-color:#c2c7d0;--pico-text-selection-color:rgba(137, 153, 249, 0.1875);--pico-muted-color:#7b8495;--pico-muted-border-color:#202632;--pico-primary:#8999f9;--pico-primary-background:#2060df;--pico-primary-border:var(--pico-primary-background);--pico-primary-underline:rgba(137, 153, 249, 0.5);--pico-primary-hover:#aeb5fb;--pico-primary-hover-background:#3c71f7;--pico-primary-hover-border:var(--pico-primary-hover-background);--pico-primary-hover-underline:var(--pico-primary-hover);--pico-primary-focus:rgba(137, 153, 249, 0.375);--pico-primary-inverse:#fff;--pico-secondary:#969eaf;--pico-secondary-background:#525f7a;--pico-secondary-border:var(--pico-secondary-background);--pico-secondary-underline:rgba(150, 158, 175, 0.5);--pico-secondary-hover:#b3b9c5;--pico-secondary-hover-background:#5d6b89;--pico-secondary-hover-border:var(--pico-secondary-hover-background);--pico-secondary-hover-underline:var(--pico-secondary-hover);--pico-secondary-focus:rgba(144, 158, 190, 0.25);--pico-secondary-inverse:#fff;--pico-contrast:#dfe3eb;--pico-contrast-background:#eff1f4;--pico-contrast-border:var(--pico-contrast-background);--pico-contrast-underline:rgba(223, 227, 235, 0.5);--pico-contrast-hover:#fff;--pico-contrast-hover-background:#fff;--pico-contrast-hover-border:var(--pico-contrast-hover-background);--pico-contrast-hover-underline:var(--pico-contrast-hover);--pico-contrast-focus:rgba(207, 213, 226, 0.25);--pico-contrast-inverse:#000;--pico-box-shadow:0.0145rem 0.029rem 0.174rem rgba(7, 8.5, 12, 0.01698),0.0335rem 0.067rem 0.402rem rgba(7, 8.5, 12, 0.024),0.0625rem 0.125rem 0.75rem rgba(7, 8.5, 12, 0.03),0.1125rem 0.225rem 1.35rem rgba(7, 8.5, 12, 0.036),0.2085rem 0.417rem 2.502rem rgba(7, 8.5, 12, 0.04302),0.5rem 1rem 6rem rgba(7, 8.5, 12, 0.06),0 0 0 0.0625rem rgba(7, 8.5, 12, 0.015);--pico-h1-color:#f0f1f3;--pico-h2-color:#e0e3e7;--pico-h3-color:#c2c7d0;--pico-h4-color:#b3b9c5;--pico-h5-color:#a4acba;--pico-h6-color:#8891a4;--pico-mark-background-color:#014063;--pico-mark-color:#fff;--pico-ins-color:#62af9a;--pico-del-color:rgb(205.5, 126, 123);--pico-blockquote-border-color:var(--pico-muted-border-color);--pico-blockquote-footer-color:var(--pico-muted-color);--pico-button-box-shadow:0 0 0 rgba(0, 0, 0, 0);--pico-button-hover-box-shadow:0 0 0 rgba(0, 0, 0, 0);--pico-table-border-color:var(--pico-muted-border-color);--pico-table-row-stripped-background-color:rgba(111, 120, 135, 0.0375);--pico-code-background-color:rgb(26, 30.5, 40.25);--pico-code-color:#8891a4;--pico-code-kbd-background-color:var(--pico-color);--pico-code-kbd-color:var(--pico-background-color);--pico-form-element-background-color:rgb(28, 33, 43.5);--pico-form-element-selected-background-color:#2a3140;--pico-form-element-border-color:#2a3140;--pico-form-element-color:#e0e3e7;--pico-form-element-placeholder-color:#8891a4;--pico-form-element-active-background-color:rgb(26, 30.5, 40.25);--pico-form-element-active-border-color:var(--pico-primary-border);--pico-form-element-focus-color:var(--pico-primary-border);--pico-form-element-disabled-opacity:0.5;--pico-form-element-invalid-border-color:rgb(149.5, 74, 80);--pico-form-element-invalid-active-border-color:rgb(183.25, 63.5, 59);--pico-form-element-invalid-focus-color:var(--pico-form-element-invalid-active-border-color);--pico-form-element-valid-border-color:#2a7b6f;--pico-form-element-valid-active-border-color:rgb(22, 137, 105.5);--pico-form-element-valid-focus-color:var(--pico-form-element-valid-active-border-color);--pico-switch-background-color:#333c4e;--pico-switch-checked-background-color:var(--pico-primary-background);--pico-switch-color:#fff;--pico-switch-thumb-box-shadow:0 0 0 rgba(0, 0, 0, 0);--pico-range-border-color:#202632;--pico-range-active-border-color:#2a3140;--pico-range-thumb-border-color:var(--pico-background-color);--pico-range-thumb-color:var(--pico-secondary-background);--pico-range-thumb-active-color:var(--pico-primary-background);--pico-accordion-border-color:var(--pico-muted-border-color);--pico-accordion-active-summary-color:var(--pico-primary-hover);--pico-accordion-close-summary-color:var(--pico-color);--pico-accordion-open-summary-color:var(--pico-muted-color);--pico-card-background-color:#181c25;--pico-card-border-color:var(--pico-card-background-color);--pico-card-box-shadow:var(--pico-box-shadow);--pico-card-sectioning-background-color:rgb(26, 30.5, 40.25);--pico-dropdown-background-color:#181c25;--pico-dropdown-border-color:#202632;--pico-dropdown-box-shadow:var(--pico-box-shadow);--pico-dropdown-color:var(--pico-color);--pico-dropdown-hover-background-color:#202632;--pico-loading-spinner-opacity:0.5;--pico-modal-overlay-background-color:rgba(7.5, 8.5, 10, 0.75);--pico-progress-background-color:#202632;--pico-progress-color:var(--pico-primary-background);--pico-tooltip-background-color:var(--pico-contrast-background);--pico-tooltip-color:var(--pico-contrast-inverse);--pico-icon-valid:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(42, 123, 111)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'%3E%3C/polyline%3E%3C/svg%3E");--pico-icon-invalid:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(149.5, 74, 80)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cline x1='12' y1='8' x2='12' y2='12'%3E%3C/line%3E%3Cline x1='12' y1='16' x2='12.01' y2='16'%3E%3C/line%3E%3C/svg%3E")}[data-theme=dark] input:is([type=submit],[type=button],[type=reset],[type=checkbox],[type=radio],[type=file]){--pico-form-element-focus-color:var(--pico-primary-focus)}[data-theme=dark] details summary[role=button].contrast:not(.outline)::after{filter:brightness(0)}[data-theme=dark] [aria-busy=true]:not(input,select,textarea).contrast:is(button,[type=submit],[type=button],[type=reset],[role=button]):not(.outline)::before{filter:brightness(0)}[type=checkbox],[type=radio],[type=range],progress{accent-color:var(--pico-primary)}*,::after,::before{box-sizing:border-box;background-repeat:no-repeat}::after,::before{text-decoration:inherit;vertical-align:inherit}:where(:host),:where(:root){-webkit-tap-highlight-color:transparent;-webkit-text-size-adjust:100%;-moz-text-size-adjust:100%;text-size-adjust:100%;background-color:var(--pico-background-color);color:var(--pico-color);font-weight:var(--pico-font-weight);font-size:var(--pico-font-size);line-height:var(--pico-line-height);font-family:var(--pico-font-family);text-underline-offset:var(--pico-text-underline-offset);text-rendering:optimizeLegibility;overflow-wrap:break-word;-moz-tab-size:4;-o-tab-size:4;tab-size:4}body{width:100%;margin:0}main{display:block}body>footer,body>header,body>main{padding-block:var(--pico-block-spacing-vertical)}section{margin-bottom:var(--pico-block-spacing-vertical)}.container,.container-fluid{width:100%;margin-right:auto;margin-left:auto;padding-right:var(--pico-spacing);padding-left:var(--pico-spacing)}@media (min-width:576px){.container{max-width:510px;padding-right:0;padding-left:0}}@media (min-width:768px){.container{max-width:700px}}@media (min-width:1024px){.container{max-width:950px}}@media (min-width:1280px){.container{max-width:1200px}}@media (min-width:1536px){.container{max-width:1450px}}.grid{grid-column-gap:var(--pico-grid-column-gap);grid-row-gap:var(--pico-grid-row-gap);display:grid;grid-template-columns:1fr}@media (min-width:768px){.grid{grid-template-columns:repeat(auto-fit,minmax(0%,1fr))}}.grid>*{min-width:0}.overflow-auto{overflow:auto}b,strong{font-weight:bolder}sub,sup{position:relative;font-size:.75em;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}address,blockquote,dl,ol,p,pre,table,ul{margin-top:0;margin-bottom:var(--pico-typography-spacing-vertical);color:var(--pico-color);font-style:normal;font-weight:var(--pico-font-weight)}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:var(--pico-typography-spacing-vertical);color:var(--pico-color);font-weight:var(--pico-font-weight);font-size:var(--pico-font-size);line-height:var(--pico-line-height);font-family:var(--pico-font-family)}h1{--pico-color:var(--pico-h1-color)}h2{--pico-color:var(--pico-h2-color)}h3{--pico-color:var(--pico-h3-color)}h4{--pico-color:var(--pico-h4-color)}h5{--pico-color:var(--pico-h5-color)}h6{--pico-color:var(--pico-h6-color)}:where(article,address,blockquote,dl,figure,form,ol,p,pre,table,ul)~:is(h1,h2,h3,h4,h5,h6){margin-top:var(--pico-typography-spacing-top)}p{margin-bottom:var(--pico-typography-spacing-vertical)}hgroup{margin-bottom:var(--pico-typography-spacing-vertical)}hgroup>*{margin-top:0;margin-bottom:0}hgroup>:not(:first-child):last-child{--pico-color:var(--pico-muted-color);--pico-font-weight:unset;font-size:1rem}:where(ol,ul) li{margin-bottom:calc(var(--pico-typography-spacing-vertical) * .25)}:where(dl,ol,ul) :where(dl,ol,ul){margin:0;margin-top:calc(var(--pico-typography-spacing-vertical) * .25)}ul li{list-style:square}mark{padding:.125rem .25rem;background-color:var(--pico-mark-background-color);color:var(--pico-mark-color);vertical-align:baseline}blockquote{display:block;margin:var(--pico-typography-spacing-vertical) 0;padding:var(--pico-spacing);border-right:none;border-left:.25rem solid var(--pico-blockquote-border-color);border-inline-start:0.25rem solid var(--pico-blockquote-border-color);border-inline-end:none}blockquote footer{margin-top:calc(var(--pico-typography-spacing-vertical) * .5);color:var(--pico-blockquote-footer-color)}abbr[title]{border-bottom:1px dotted;text-decoration:none;cursor:help}ins{color:var(--pico-ins-color);text-decoration:none}del{color:var(--pico-del-color)}::-moz-selection{background-color:var(--pico-text-selection-color)}::selection{background-color:var(--pico-text-selection-color)}:where(a:not([role=button])),[role=link]{--pico-color:var(--pico-primary);--pico-background-color:transparent;--pico-underline:var(--pico-primary-underline);outline:0;background-color:var(--pico-background-color);color:var(--pico-color);-webkit-text-decoration:var(--pico-text-decoration);text-decoration:var(--pico-text-decoration);text-decoration-color:var(--pico-underline);text-underline-offset:0.125em;transition:background-color var(--pico-transition),color var(--pico-transition),box-shadow var(--pico-transition),-webkit-text-decoration var(--pico-transition);transition:background-color var(--pico-transition),color var(--pico-transition),text-decoration var(--pico-transition),box-shadow var(--pico-transition);transition:background-color var(--pico-transition),color var(--pico-transition),text-decoration var(--pico-transition),box-shadow var(--pico-transition),-webkit-text-decoration var(--pico-transition)}:where(a:not([role=button])):is([aria-current]:not([aria-current=false]),:hover,:active,:focus),[role=link]:is([aria-current]:not([aria-current=false]),:hover,:active,:focus){--pico-color:var(--pico-primary-hover);--pico-underline:var(--pico-primary-hover-underline);--pico-text-decoration:underline}:where(a:not([role=button])):focus-visible,[role=link]:focus-visible{box-shadow:0 0 0 var(--pico-outline-width) var(--pico-primary-focus)}:where(a:not([role=button])).secondary,[role=link].secondary{--pico-color:var(--pico-secondary);--pico-underline:var(--pico-secondary-underline)}:where(a:not([role=button])).secondary:is([aria-current]:not([aria-current=false]),:hover,:active,:focus),[role=link].secondary:is([aria-current]:not([aria-current=false]),:hover,:active,:focus){--pico-color:var(--pico-secondary-hover);--pico-underline:var(--pico-secondary-hover-underline)}:where(a:not([role=button])).contrast,[role=link].contrast{--pico-color:var(--pico-contrast);--pico-underline:var(--pico-contrast-underline)}:where(a:not([role=button])).contrast:is([aria-current]:not([aria-current=false]),:hover,:active,:focus),[role=link].contrast:is([aria-current]:not([aria-current=false]),:hover,:active,:focus){--pico-color:var(--pico-contrast-hover);--pico-underline:var(--pico-contrast-hover-underline)}a[role=button]{display:inline-block}button{margin:0;overflow:visible;font-family:inherit;text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[role=button],[type=button],[type=file]::file-selector-button,[type=reset],[type=submit],button{--pico-background-color:var(--pico-primary-background);--pico-border-color:var(--pico-primary-border);--pico-color:var(--pico-primary-inverse);--pico-box-shadow:var(--pico-button-box-shadow, 0 0 0 rgba(0, 0, 0, 0));padding:var(--pico-form-element-spacing-vertical) var(--pico-form-element-spacing-horizontal);border:var(--pico-border-width) solid var(--pico-border-color);border-radius:var(--pico-border-radius);outline:0;background-color:var(--pico-background-color);box-shadow:var(--pico-box-shadow);color:var(--pico-color);font-weight:var(--pico-font-weight);font-size:1rem;line-height:var(--pico-line-height);text-align:center;text-decoration:none;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;user-select:none;transition:background-color var(--pico-transition),border-color var(--pico-transition),color var(--pico-transition),box-shadow var(--pico-transition)}[role=button]:is(:hover,:active,:focus),[role=button]:is([aria-current]:not([aria-current=false])),[type=button]:is(:hover,:active,:focus),[type=button]:is([aria-current]:not([aria-current=false])),[type=file]::file-selector-button:is(:hover,:active,:focus),[type=file]::file-selector-button:is([aria-current]:not([aria-current=false])),[type=reset]:is(:hover,:active,:focus),[type=reset]:is([aria-current]:not([aria-current=false])),[type=submit]:is(:hover,:active,:focus),[type=submit]:is([aria-current]:not([aria-current=false])),button:is(:hover,:active,:focus),button:is([aria-current]:not([aria-current=false])){--pico-background-color:var(--pico-primary-hover-background);--pico-border-color:var(--pico-primary-hover-border);--pico-box-shadow:var(--pico-button-hover-box-shadow, 0 0 0 rgba(0, 0, 0, 0));--pico-color:var(--pico-primary-inverse)}[role=button]:focus,[role=button]:is([aria-current]:not([aria-current=false])):focus,[type=button]:focus,[type=button]:is([aria-current]:not([aria-current=false])):focus,[type=file]::file-selector-button:focus,[type=file]::file-selector-button:is([aria-current]:not([aria-current=false])):focus,[type=reset]:focus,[type=reset]:is([aria-current]:not([aria-current=false])):focus,[type=submit]:focus,[type=submit]:is([aria-current]:not([aria-current=false])):focus,button:focus,button:is([aria-current]:not([aria-current=false])):focus{--pico-box-shadow:var(--pico-button-hover-box-shadow, 0 0 0 rgba(0, 0, 0, 0)),0 0 0 var(--pico-outline-width) var(--pico-primary-focus)}[type=button],[type=reset],[type=submit]{margin-bottom:var(--pico-spacing)}:is(button,[type=submit],[type=button],[role=button]).secondary,[type=file]::file-selector-button,[type=reset]{--pico-background-color:var(--pico-secondary-background);--pico-border-color:var(--pico-secondary-border);--pico-color:var(--pico-secondary-inverse);cursor:pointer}:is(button,[type=submit],[type=button],[role=button]).secondary:is([aria-current]:not([aria-current=false]),:hover,:active,:focus),[type=file]::file-selector-button:is([aria-current]:not([aria-current=false]),:hover,:active,:focus),[type=reset]:is([aria-current]:not([aria-current=false]),:hover,:active,:focus){--pico-background-color:var(--pico-secondary-hover-background);--pico-border-color:var(--pico-secondary-hover-border);--pico-color:var(--pico-secondary-inverse)}:is(button,[type=submit],[type=button],[role=button]).secondary:focus,:is(button,[type=submit],[type=button],[role=button]).secondary:is([aria-current]:not([aria-current=false])):focus,[type=file]::file-selector-button:focus,[type=file]::file-selector-button:is([aria-current]:not([aria-current=false])):focus,[type=reset]:focus,[type=reset]:is([aria-current]:not([aria-current=false])):focus{--pico-box-shadow:var(--pico-button-hover-box-shadow, 0 0 0 rgba(0, 0, 0, 0)),0 0 0 var(--pico-outline-width) var(--pico-secondary-focus)}:is(button,[type=submit],[type=button],[role=button]).contrast{--pico-background-color:var(--pico-contrast-background);--pico-border-color:var(--pico-contrast-border);--pico-color:var(--pico-contrast-inverse)}:is(button,[type=submit],[type=button],[role=button]).contrast:is([aria-current]:not([aria-current=false]),:hover,:active,:focus){--pico-background-color:var(--pico-contrast-hover-background);--pico-border-color:var(--pico-contrast-hover-border);--pico-color:var(--pico-contrast-inverse)}:is(button,[type=submit],[type=button],[role=button]).contrast:focus,:is(button,[type=submit],[type=button],[role=button]).contrast:is([aria-current]:not([aria-current=false])):focus{--pico-box-shadow:var(--pico-button-hover-box-shadow, 0 0 0 rgba(0, 0, 0, 0)),0 0 0 var(--pico-outline-width) var(--pico-contrast-focus)}:is(button,[type=submit],[type=button],[role=button]).outline,[type=reset].outline{--pico-background-color:transparent;--pico-color:var(--pico-primary);--pico-border-color:var(--pico-primary)}:is(button,[type=submit],[type=button],[role=button]).outline:is([aria-current]:not([aria-current=false]),:hover,:active,:focus),[type=reset].outline:is([aria-current]:not([aria-current=false]),:hover,:active,:focus){--pico-background-color:transparent;--pico-color:var(--pico-primary-hover);--pico-border-color:var(--pico-primary-hover)}:is(button,[type=submit],[type=button],[role=button]).outline.secondary,[type=reset].outline{--pico-color:var(--pico-secondary);--pico-border-color:var(--pico-secondary)}:is(button,[type=submit],[type=button],[role=button]).outline.secondary:is([aria-current]:not([aria-current=false]),:hover,:active,:focus),[type=reset].outline:is([aria-current]:not([aria-current=false]),:hover,:active,:focus){--pico-color:var(--pico-secondary-hover);--pico-border-color:var(--pico-secondary-hover)}:is(button,[type=submit],[type=button],[role=button]).outline.contrast{--pico-color:var(--pico-contrast);--pico-border-color:var(--pico-contrast)}:is(button,[type=submit],[type=button],[role=button]).outline.contrast:is([aria-current]:not([aria-current=false]),:hover,:active,:focus){--pico-color:var(--pico-contrast-hover);--pico-border-color:var(--pico-contrast-hover)}:where(button,[type=submit],[type=reset],[type=button],[role=button])[disabled],:where(fieldset[disabled]) :is(button,[type=submit],[type=button],[type=reset],[role=button]){opacity:.5;pointer-events:none}:where(table){width:100%;border-collapse:collapse;border-spacing:0;text-indent:0}td,th{padding:calc(var(--pico-spacing)/ 2) var(--pico-spacing);border-bottom:var(--pico-border-width) solid var(--pico-table-border-color);background-color:var(--pico-background-color);color:var(--pico-color);font-weight:var(--pico-font-weight);text-align:left;text-align:start}tfoot td,tfoot th{border-top:var(--pico-border-width) solid var(--pico-table-border-color);border-bottom:0}table.striped tbody tr:nth-child(odd) td,table.striped tbody tr:nth-child(odd) th{background-color:var(--pico-table-row-stripped-background-color)}:where(audio,canvas,iframe,img,svg,video){vertical-align:middle}audio,video{display:inline-block}audio:not([controls]){display:none;height:0}:where(iframe){border-style:none}img{max-width:100%;height:auto;border-style:none}:where(svg:not([fill])){fill:currentColor}svg:not(:host),svg:not(:root){overflow:hidden}code,kbd,pre,samp{font-size:.875em;font-family:var(--pico-font-family)}pre code,pre samp{font-size:inherit;font-family:inherit}pre{-ms-overflow-style:scrollbar;overflow:auto}code,kbd,pre,samp{border-radius:var(--pico-border-radius);background:var(--pico-code-background-color);color:var(--pico-code-color);font-weight:var(--pico-font-weight);line-height:initial}code,kbd,samp{display:inline-block;padding:.375rem}pre{display:block;margin-bottom:var(--pico-spacing);overflow-x:auto}pre>code,pre>samp{display:block;padding:var(--pico-spacing);background:0 0;line-height:var(--pico-line-height)}kbd{background-color:var(--pico-code-kbd-background-color);color:var(--pico-code-kbd-color);vertical-align:baseline}figure{display:block;margin:0;padding:0}figure figcaption{padding:calc(var(--pico-spacing) * .5) 0;color:var(--pico-muted-color)}hr{height:0;margin:var(--pico-typography-spacing-vertical) 0;border:0;border-top:1px solid var(--pico-muted-border-color);color:inherit}[hidden],template{display:none!important}canvas{display:inline-block}input,optgroup,select,textarea{margin:0;font-size:1rem;line-height:var(--pico-line-height);font-family:inherit;letter-spacing:inherit}input{overflow:visible}select{text-transform:none}legend{max-width:100%;padding:0;color:inherit;white-space:normal}textarea{overflow:auto}[type=checkbox],[type=radio]{padding:0}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}::-moz-focus-inner{padding:0;border-style:none}:-moz-focusring{outline:0}:-moz-ui-invalid{box-shadow:none}::-ms-expand{display:none}[type=file],[type=range]{padding:0;border-width:0}input:not([type=checkbox],[type=radio],[type=range]){height:calc(1rem * var(--pico-line-height) + var(--pico-form-element-spacing-vertical) * 2 + var(--pico-border-width) * 2)}fieldset{width:100%;margin:0;margin-bottom:var(--pico-spacing);padding:0;border:0}fieldset legend,label{display:block;margin-bottom:calc(var(--pico-spacing) * .375);color:var(--pico-color);font-weight:var(--pico-form-label-font-weight,var(--pico-font-weight))}fieldset legend{margin-bottom:calc(var(--pico-spacing) * .5)}button[type=submit],input:not([type=checkbox],[type=radio]),select,textarea{width:100%}input:not([type=checkbox],[type=radio],[type=range],[type=file]),select,textarea{-webkit-appearance:none;-moz-appearance:none;appearance:none;padding:var(--pico-form-element-spacing-vertical) var(--pico-form-element-spacing-horizontal)}input,select,textarea{--pico-background-color:var(--pico-form-element-background-color);--pico-border-color:var(--pico-form-element-border-color);--pico-color:var(--pico-form-element-color);--pico-box-shadow:none;border:var(--pico-border-width) solid var(--pico-border-color);border-radius:var(--pico-border-radius);outline:0;background-color:var(--pico-background-color);box-shadow:var(--pico-box-shadow);color:var(--pico-color);font-weight:var(--pico-font-weight);transition:background-color var(--pico-transition),border-color var(--pico-transition),color var(--pico-transition),box-shadow var(--pico-transition)}:where(select,textarea):not([readonly]):is(:active,:focus),input:not([type=submit],[type=button],[type=reset],[type=checkbox],[type=radio],[readonly]):is(:active,:focus){--pico-background-color:var(--pico-form-element-active-background-color)}:where(select,textarea):not([readonly]):is(:active,:focus),input:not([type=submit],[type=button],[type=reset],[role=switch],[readonly]):is(:active,:focus){--pico-border-color:var(--pico-form-element-active-border-color)}:where(select,textarea):not([readonly]):focus,input:not([type=submit],[type=button],[type=reset],[type=range],[type=file],[readonly]):focus{--pico-box-shadow:0 0 0 var(--pico-outline-width) var(--pico-form-element-focus-color)}:where(fieldset[disabled]) :is(input:not([type=submit],[type=button],[type=reset]),select,textarea),input:not([type=submit],[type=button],[type=reset])[disabled],label[aria-disabled=true],select[disabled],textarea[disabled]{opacity:var(--pico-form-element-disabled-opacity);pointer-events:none}label[aria-disabled=true] input[disabled]{opacity:1}:where(input,select,textarea):not([type=checkbox],[type=radio],[type=date],[type=datetime-local],[type=month],[type=time],[type=week],[type=range])[aria-invalid]{padding-right:calc(var(--pico-form-element-spacing-horizontal) + 1.5rem)!important;padding-left:var(--pico-form-element-spacing-horizontal);padding-inline-start:var(--pico-form-element-spacing-horizontal)!important;padding-inline-end:calc(var(--pico-form-element-spacing-horizontal) + 1.5rem)!important;background-position:center right .75rem;background-size:1rem auto;background-repeat:no-repeat}:where(input,select,textarea):not([type=checkbox],[type=radio],[type=date],[type=datetime-local],[type=month],[type=time],[type=week],[type=range])[aria-invalid=false]:not(select){background-image:var(--pico-icon-valid)}:where(input,select,textarea):not([type=checkbox],[type=radio],[type=date],[type=datetime-local],[type=month],[type=time],[type=week],[type=range])[aria-invalid=true]:not(select){background-image:var(--pico-icon-invalid)}:where(input,select,textarea)[aria-invalid=false]{--pico-border-color:var(--pico-form-element-valid-border-color)}:where(input,select,textarea)[aria-invalid=false]:is(:active,:focus){--pico-border-color:var(--pico-form-element-valid-active-border-color)!important}:where(input,select,textarea)[aria-invalid=false]:is(:active,:focus):not([type=checkbox],[type=radio]){--pico-box-shadow:0 0 0 var(--pico-outline-width) var(--pico-form-element-valid-focus-color)!important}:where(input,select,textarea)[aria-invalid=true]{--pico-border-color:var(--pico-form-element-invalid-border-color)}:where(input,select,textarea)[aria-invalid=true]:is(:active,:focus){--pico-border-color:var(--pico-form-element-invalid-active-border-color)!important}:where(input,select,textarea)[aria-invalid=true]:is(:active,:focus):not([type=checkbox],[type=radio]){--pico-box-shadow:0 0 0 var(--pico-outline-width) var(--pico-form-element-invalid-focus-color)!important}[dir=rtl] :where(input,select,textarea):not([type=checkbox],[type=radio]):is([aria-invalid],[aria-invalid=true],[aria-invalid=false]){background-position:center left .75rem}input::-webkit-input-placeholder,input::placeholder,select:invalid,textarea::-webkit-input-placeholder,textarea::placeholder{color:var(--pico-form-element-placeholder-color);opacity:1}input:not([type=checkbox],[type=radio]),select,textarea{margin-bottom:var(--pico-spacing)}select::-ms-expand{border:0;background-color:transparent}select:not([multiple],[size]){padding-right:calc(var(--pico-form-element-spacing-horizontal) + 1.5rem);padding-left:var(--pico-form-element-spacing-horizontal);padding-inline-start:var(--pico-form-element-spacing-horizontal);padding-inline-end:calc(var(--pico-form-element-spacing-horizontal) + 1.5rem);background-image:var(--pico-icon-chevron);background-position:center right .75rem;background-size:1rem auto;background-repeat:no-repeat}select[multiple] option:checked{background:var(--pico-form-element-selected-background-color);color:var(--pico-form-element-color)}[dir=rtl] select:not([multiple],[size]){background-position:center left .75rem}textarea{display:block;resize:vertical}textarea[aria-invalid]{--pico-icon-height:calc(1rem * var(--pico-line-height) + var(--pico-form-element-spacing-vertical) * 2 + var(--pico-border-width) * 2);background-position:top right .75rem!important;background-size:1rem var(--pico-icon-height)!important}:where(input,select,textarea,fieldset,.grid)+small{display:block;width:100%;margin-top:calc(var(--pico-spacing) * -.75);margin-bottom:var(--pico-spacing);color:var(--pico-muted-color)}:where(input,select,textarea,fieldset,.grid)[aria-invalid=false]+small{color:var(--pico-ins-color)}:where(input,select,textarea,fieldset,.grid)[aria-invalid=true]+small{color:var(--pico-del-color)}label>:where(input,select,textarea){margin-top:calc(var(--pico-spacing) * .25)}label:has([type=checkbox],[type=radio]){width:-moz-fit-content;width:fit-content;cursor:pointer}[type=checkbox],[type=radio]{-webkit-appearance:none;-moz-appearance:none;appearance:none;width:1.25em;height:1.25em;margin-top:-.125em;margin-inline-end:.5em;border-width:var(--pico-border-width);vertical-align:middle;cursor:pointer}[type=checkbox]::-ms-check,[type=radio]::-ms-check{display:none}[type=checkbox]:checked,[type=checkbox]:checked:active,[type=checkbox]:checked:focus,[type=radio]:checked,[type=radio]:checked:active,[type=radio]:checked:focus{--pico-background-color:var(--pico-primary-background);--pico-border-color:var(--pico-primary-border);background-image:var(--pico-icon-checkbox);background-position:center;background-size:.75em auto;background-repeat:no-repeat}[type=checkbox]~label,[type=radio]~label{display:inline-block;margin-bottom:0;cursor:pointer}[type=checkbox]~label:not(:last-of-type),[type=radio]~label:not(:last-of-type){margin-inline-end:1em}[type=checkbox]:indeterminate{--pico-background-color:var(--pico-primary-background);--pico-border-color:var(--pico-primary-border);background-image:var(--pico-icon-minus);background-position:center;background-size:.75em auto;background-repeat:no-repeat}[type=radio]{border-radius:50%}[type=radio]:checked,[type=radio]:checked:active,[type=radio]:checked:focus{--pico-background-color:var(--pico-primary-inverse);border-width:.35em;background-image:none}[type=checkbox][role=switch]{--pico-background-color:var(--pico-switch-background-color);--pico-color:var(--pico-switch-color);width:2.25em;height:1.25em;border:var(--pico-border-width) solid var(--pico-border-color);border-radius:1.25em;background-color:var(--pico-background-color);line-height:1.25em}[type=checkbox][role=switch]:not([aria-invalid]){--pico-border-color:var(--pico-switch-background-color)}[type=checkbox][role=switch]:before{display:block;aspect-ratio:1;height:100%;border-radius:50%;background-color:var(--pico-color);box-shadow:var(--pico-switch-thumb-box-shadow);content:"";transition:margin .1s ease-in-out}[type=checkbox][role=switch]:focus{--pico-background-color:var(--pico-switch-background-color);--pico-border-color:var(--pico-switch-background-color)}[type=checkbox][role=switch]:checked{--pico-background-color:var(--pico-switch-checked-background-color);--pico-border-color:var(--pico-switch-checked-background-color);background-image:none}[type=checkbox][role=switch]:checked::before{margin-inline-start:calc(2.25em - 1.25em)}[type=checkbox][role=switch][disabled]{--pico-background-color:var(--pico-border-color)}[type=checkbox][aria-invalid=false]:checked,[type=checkbox][aria-invalid=false]:checked:active,[type=checkbox][aria-invalid=false]:checked:focus,[type=checkbox][role=switch][aria-invalid=false]:checked,[type=checkbox][role=switch][aria-invalid=false]:checked:active,[type=checkbox][role=switch][aria-invalid=false]:checked:focus{--pico-background-color:var(--pico-form-element-valid-border-color)}[type=checkbox]:checked:active[aria-invalid=true],[type=checkbox]:checked:focus[aria-invalid=true],[type=checkbox]:checked[aria-invalid=true],[type=checkbox][role=switch]:checked:active[aria-invalid=true],[type=checkbox][role=switch]:checked:focus[aria-invalid=true],[type=checkbox][role=switch]:checked[aria-invalid=true]{--pico-background-color:var(--pico-form-element-invalid-border-color)}[type=checkbox][aria-invalid=false]:checked,[type=checkbox][aria-invalid=false]:checked:active,[type=checkbox][aria-invalid=false]:checked:focus,[type=checkbox][role=switch][aria-invalid=false]:checked,[type=checkbox][role=switch][aria-invalid=false]:checked:active,[type=checkbox][role=switch][aria-invalid=false]:checked:focus,[type=radio][aria-invalid=false]:checked,[type=radio][aria-invalid=false]:checked:active,[type=radio][aria-invalid=false]:checked:focus{--pico-border-color:var(--pico-form-element-valid-border-color)}[type=checkbox]:checked:active[aria-invalid=true],[type=checkbox]:checked:focus[aria-invalid=true],[type=checkbox]:checked[aria-invalid=true],[type=checkbox][role=switch]:checked:active[aria-invalid=true],[type=checkbox][role=switch]:checked:focus[aria-invalid=true],[type=checkbox][role=switch]:checked[aria-invalid=true],[type=radio]:checked:active[aria-invalid=true],[type=radio]:checked:focus[aria-invalid=true],[type=radio]:checked[aria-invalid=true]{--pico-border-color:var(--pico-form-element-invalid-border-color)}[type=color]::-webkit-color-swatch-wrapper{padding:0}[type=color]::-moz-focus-inner{padding:0}[type=color]::-webkit-color-swatch{border:0;border-radius:calc(var(--pico-border-radius) * .5)}[type=color]::-moz-color-swatch{border:0;border-radius:calc(var(--pico-border-radius) * .5)}input:not([type=checkbox],[type=radio],[type=range],[type=file]):is([type=date],[type=datetime-local],[type=month],[type=time],[type=week]){--pico-icon-position:0.75rem;--pico-icon-width:1rem;padding-right:calc(var(--pico-icon-width) + var(--pico-icon-position));background-image:var(--pico-icon-date);background-position:center right var(--pico-icon-position);background-size:var(--pico-icon-width) auto;background-repeat:no-repeat}input:not([type=checkbox],[type=radio],[type=range],[type=file])[type=time]{background-image:var(--pico-icon-time)}[type=date]::-webkit-calendar-picker-indicator,[type=datetime-local]::-webkit-calendar-picker-indicator,[type=month]::-webkit-calendar-picker-indicator,[type=time]::-webkit-calendar-picker-indicator,[type=week]::-webkit-calendar-picker-indicator{width:var(--pico-icon-width);margin-right:calc(var(--pico-icon-width) * -1);margin-left:var(--pico-icon-position);opacity:0}@-moz-document url-prefix(){[type=date],[type=datetime-local],[type=month],[type=time],[type=week]{padding-right:var(--pico-form-element-spacing-horizontal)!important;background-image:none!important}}[dir=rtl] :is([type=date],[type=datetime-local],[type=month],[type=time],[type=week]){text-align:right}[type=file]{--pico-color:var(--pico-muted-color);margin-left:calc(var(--pico-outline-width) * -1);padding:calc(var(--pico-form-element-spacing-vertical) * .5) 0;padding-left:var(--pico-outline-width);border:0;border-radius:0;background:0 0}[type=file]::file-selector-button{margin-right:calc(var(--pico-spacing)/ 2);padding:calc(var(--pico-form-element-spacing-vertical) * .5) var(--pico-form-element-spacing-horizontal)}[type=file]:is(:hover,:active,:focus)::file-selector-button{--pico-background-color:var(--pico-secondary-hover-background);--pico-border-color:var(--pico-secondary-hover-border)}[type=file]:focus::file-selector-button{--pico-box-shadow:var(--pico-button-hover-box-shadow, 0 0 0 rgba(0, 0, 0, 0)),0 0 0 var(--pico-outline-width) var(--pico-secondary-focus)}[type=range]{-webkit-appearance:none;-moz-appearance:none;appearance:none;width:100%;height:1.25rem;background:0 0}[type=range]::-webkit-slider-runnable-track{width:100%;height:.375rem;border-radius:var(--pico-border-radius);background-color:var(--pico-range-border-color);-webkit-transition:background-color var(--pico-transition),box-shadow var(--pico-transition);transition:background-color var(--pico-transition),box-shadow var(--pico-transition)}[type=range]::-moz-range-track{width:100%;height:.375rem;border-radius:var(--pico-border-radius);background-color:var(--pico-range-border-color);-moz-transition:background-color var(--pico-transition),box-shadow var(--pico-transition);transition:background-color var(--pico-transition),box-shadow var(--pico-transition)}[type=range]::-ms-track{width:100%;height:.375rem;border-radius:var(--pico-border-radius);background-color:var(--pico-range-border-color);-ms-transition:background-color var(--pico-transition),box-shadow var(--pico-transition);transition:background-color var(--pico-transition),box-shadow var(--pico-transition)}[type=range]::-webkit-slider-thumb{-webkit-appearance:none;width:1.25rem;height:1.25rem;margin-top:-.4375rem;border:2px solid var(--pico-range-thumb-border-color);border-radius:50%;background-color:var(--pico-range-thumb-color);cursor:pointer;-webkit-transition:background-color var(--pico-transition),transform var(--pico-transition);transition:background-color var(--pico-transition),transform var(--pico-transition)}[type=range]::-moz-range-thumb{-webkit-appearance:none;width:1.25rem;height:1.25rem;margin-top:-.4375rem;border:2px solid var(--pico-range-thumb-border-color);border-radius:50%;background-color:var(--pico-range-thumb-color);cursor:pointer;-moz-transition:background-color var(--pico-transition),transform var(--pico-transition);transition:background-color var(--pico-transition),transform var(--pico-transition)}[type=range]::-ms-thumb{-webkit-appearance:none;width:1.25rem;height:1.25rem;margin-top:-.4375rem;border:2px solid var(--pico-range-thumb-border-color);border-radius:50%;background-color:var(--pico-range-thumb-color);cursor:pointer;-ms-transition:background-color var(--pico-transition),transform var(--pico-transition);transition:background-color var(--pico-transition),transform var(--pico-transition)}[type=range]:active,[type=range]:focus-within{--pico-range-border-color:var(--pico-range-active-border-color);--pico-range-thumb-color:var(--pico-range-thumb-active-color)}[type=range]:active::-webkit-slider-thumb{transform:scale(1.25)}[type=range]:active::-moz-range-thumb{transform:scale(1.25)}[type=range]:active::-ms-thumb{transform:scale(1.25)}input:not([type=checkbox],[type=radio],[type=range],[type=file])[type=search]{padding-inline-start:calc(var(--pico-form-element-spacing-horizontal) + 1.75rem);background-image:var(--pico-icon-search);background-position:center left calc(var(--pico-form-element-spacing-horizontal) + .125rem);background-size:1rem auto;background-repeat:no-repeat}input:not([type=checkbox],[type=radio],[type=range],[type=file])[type=search][aria-invalid]{padding-inline-start:calc(var(--pico-form-element-spacing-horizontal) + 1.75rem)!important;background-position:center left 1.125rem,center right .75rem}input:not([type=checkbox],[type=radio],[type=range],[type=file])[type=search][aria-invalid=false]{background-image:var(--pico-icon-search),var(--pico-icon-valid)}input:not([type=checkbox],[type=radio],[type=range],[type=file])[type=search][aria-invalid=true]{background-image:var(--pico-icon-search),var(--pico-icon-invalid)}[dir=rtl] :where(input):not([type=checkbox],[type=radio],[type=range],[type=file])[type=search]{background-position:center right 1.125rem}[dir=rtl] :where(input):not([type=checkbox],[type=radio],[type=range],[type=file])[type=search][aria-invalid]{background-position:center right 1.125rem,center left .75rem}details{display:block;margin-bottom:var(--pico-spacing)}details summary{line-height:1rem;list-style-type:none;cursor:pointer;transition:color var(--pico-transition)}details summary:not([role]){color:var(--pico-accordion-close-summary-color)}details summary::-webkit-details-marker{display:none}details summary::marker{display:none}details summary::-moz-list-bullet{list-style-type:none}details summary::after{display:block;width:1rem;height:1rem;margin-inline-start:calc(var(--pico-spacing,1rem) * .5);float:right;transform:rotate(-90deg);background-image:var(--pico-icon-chevron);background-position:right center;background-size:1rem auto;background-repeat:no-repeat;content:"";transition:transform var(--pico-transition)}details summary:focus{outline:0}details summary:focus:not([role]){color:var(--pico-accordion-active-summary-color)}details summary:focus-visible:not([role]){outline:var(--pico-outline-width) solid var(--pico-primary-focus);outline-offset:calc(var(--pico-spacing,1rem) * 0.5);color:var(--pico-primary)}details summary[role=button]{width:100%;text-align:left}details summary[role=button]::after{height:calc(1rem * var(--pico-line-height,1.5))}details[open]>summary{margin-bottom:var(--pico-spacing)}details[open]>summary:not([role]):not(:focus){color:var(--pico-accordion-open-summary-color)}details[open]>summary::after{transform:rotate(0)}[dir=rtl] details summary{text-align:right}[dir=rtl] details summary::after{float:left;background-position:left center}article{margin-bottom:var(--pico-block-spacing-vertical);padding:var(--pico-block-spacing-vertical) var(--pico-block-spacing-horizontal);border-radius:var(--pico-border-radius);background:var(--pico-card-background-color);box-shadow:var(--pico-card-box-shadow)}article>footer,article>header{margin-right:calc(var(--pico-block-spacing-horizontal) * -1);margin-left:calc(var(--pico-block-spacing-horizontal) * -1);padding:calc(var(--pico-block-spacing-vertical) * .66) var(--pico-block-spacing-horizontal);background-color:var(--pico-card-sectioning-background-color)}article>header{margin-top:calc(var(--pico-block-spacing-vertical) * -1);margin-bottom:var(--pico-block-spacing-vertical);border-bottom:var(--pico-border-width) solid var(--pico-card-border-color);border-top-right-radius:var(--pico-border-radius);border-top-left-radius:var(--pico-border-radius)}article>footer{margin-top:var(--pico-block-spacing-vertical);margin-bottom:calc(var(--pico-block-spacing-vertical) * -1);border-top:var(--pico-border-width) solid var(--pico-card-border-color);border-bottom-right-radius:var(--pico-border-radius);border-bottom-left-radius:var(--pico-border-radius)}details.dropdown{position:relative;border-bottom:none}details.dropdown>a::after,details.dropdown>button::after,details.dropdown>summary::after{display:block;width:1rem;height:calc(1rem * var(--pico-line-height,1.5));margin-inline-start:.25rem;float:right;transform:rotate(0) translateX(.2rem);background-image:var(--pico-icon-chevron);background-position:right center;background-size:1rem auto;background-repeat:no-repeat;content:""}nav details.dropdown{margin-bottom:0}details.dropdown>summary:not([role]){height:calc(1rem * var(--pico-line-height) + var(--pico-form-element-spacing-vertical) * 2 + var(--pico-border-width) * 2);padding:var(--pico-form-element-spacing-vertical) var(--pico-form-element-spacing-horizontal);border:var(--pico-border-width) solid var(--pico-form-element-border-color);border-radius:var(--pico-border-radius);background-color:var(--pico-form-element-background-color);color:var(--pico-form-element-placeholder-color);line-height:inherit;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;user-select:none;transition:background-color var(--pico-transition),border-color var(--pico-transition),color var(--pico-transition),box-shadow var(--pico-transition)}details.dropdown>summary:not([role]):active,details.dropdown>summary:not([role]):focus{border-color:var(--pico-form-element-active-border-color);background-color:var(--pico-form-element-active-background-color)}details.dropdown>summary:not([role]):focus{box-shadow:0 0 0 var(--pico-outline-width) var(--pico-form-element-focus-color)}details.dropdown>summary:not([role]):focus-visible{outline:0}details.dropdown>summary:not([role])[aria-invalid=false]{--pico-form-element-border-color:var(--pico-form-element-valid-border-color);--pico-form-element-active-border-color:var(--pico-form-element-valid-focus-color);--pico-form-element-focus-color:var(--pico-form-element-valid-focus-color)}details.dropdown>summary:not([role])[aria-invalid=true]{--pico-form-element-border-color:var(--pico-form-element-invalid-border-color);--pico-form-element-active-border-color:var(--pico-form-element-invalid-focus-color);--pico-form-element-focus-color:var(--pico-form-element-invalid-focus-color)}nav details.dropdown{display:inline;margin:calc(var(--pico-nav-element-spacing-vertical) * -1) 0}nav details.dropdown>summary::after{transform:rotate(0) translateX(0)}nav details.dropdown>summary:not([role]){height:calc(1rem * var(--pico-line-height) + var(--pico-nav-link-spacing-vertical) * 2);padding:calc(var(--pico-nav-link-spacing-vertical) - var(--pico-border-width) * 2) var(--pico-nav-link-spacing-horizontal)}nav details.dropdown>summary:not([role]):focus-visible{box-shadow:0 0 0 var(--pico-outline-width) var(--pico-primary-focus)}details.dropdown>summary+ul{display:flex;z-index:99;position:absolute;left:0;flex-direction:column;width:100%;min-width:-moz-fit-content;min-width:fit-content;margin:0;margin-top:var(--pico-outline-width);padding:0;border:var(--pico-border-width) solid var(--pico-dropdown-border-color);border-radius:var(--pico-border-radius);background-color:var(--pico-dropdown-background-color);box-shadow:var(--pico-dropdown-box-shadow);color:var(--pico-dropdown-color);white-space:nowrap;opacity:0;transition:opacity var(--pico-transition),transform 0s ease-in-out 1s}details.dropdown>summary+ul[dir=rtl]{right:0;left:auto}details.dropdown>summary+ul li{width:100%;margin-bottom:0;padding:calc(var(--pico-form-element-spacing-vertical) * .5) var(--pico-form-element-spacing-horizontal);list-style:none}details.dropdown>summary+ul li:first-of-type{margin-top:calc(var(--pico-form-element-spacing-vertical) * .5)}details.dropdown>summary+ul li:last-of-type{margin-bottom:calc(var(--pico-form-element-spacing-vertical) * .5)}details.dropdown>summary+ul li a{display:block;margin:calc(var(--pico-form-element-spacing-vertical) * -.5) calc(var(--pico-form-element-spacing-horizontal) * -1);padding:calc(var(--pico-form-element-spacing-vertical) * .5) var(--pico-form-element-spacing-horizontal);overflow:hidden;border-radius:0;color:var(--pico-dropdown-color);text-decoration:none;text-overflow:ellipsis}details.dropdown>summary+ul li a:active,details.dropdown>summary+ul li a:focus,details.dropdown>summary+ul li a:focus-visible,details.dropdown>summary+ul li a:hover,details.dropdown>summary+ul li a[aria-current]:not([aria-current=false]){background-color:var(--pico-dropdown-hover-background-color)}details.dropdown>summary+ul li label{width:100%}details.dropdown>summary+ul li:has(label):hover{background-color:var(--pico-dropdown-hover-background-color)}details.dropdown[open]>summary{margin-bottom:0}details.dropdown[open]>summary+ul{transform:scaleY(1);opacity:1;transition:opacity var(--pico-transition),transform 0s ease-in-out 0s}details.dropdown[open]>summary::before{display:block;z-index:1;position:fixed;width:100vw;height:100vh;inset:0;background:0 0;content:"";cursor:default}label>details.dropdown{margin-top:calc(var(--pico-spacing) * .25)}[role=group],[role=search]{display:inline-flex;position:relative;width:100%;margin-bottom:var(--pico-spacing);border-radius:var(--pico-border-radius);box-shadow:var(--pico-group-box-shadow,0 0 0 transparent);vertical-align:middle;transition:box-shadow var(--pico-transition)}[role=group] input:not([type=checkbox],[type=radio]),[role=group] select,[role=group]>*,[role=search] input:not([type=checkbox],[type=radio]),[role=search] select,[role=search]>*{position:relative;flex:1 1 auto;margin-bottom:0}[role=group] input:not([type=checkbox],[type=radio]):not(:first-child),[role=group] select:not(:first-child),[role=group]>:not(:first-child),[role=search] input:not([type=checkbox],[type=radio]):not(:first-child),[role=search] select:not(:first-child),[role=search]>:not(:first-child){margin-left:0;border-top-left-radius:0;border-bottom-left-radius:0}[role=group] input:not([type=checkbox],[type=radio]):not(:last-child),[role=group] select:not(:last-child),[role=group]>:not(:last-child),[role=search] input:not([type=checkbox],[type=radio]):not(:last-child),[role=search] select:not(:last-child),[role=search]>:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}[role=group] input:not([type=checkbox],[type=radio]):focus,[role=group] select:focus,[role=group]>:focus,[role=search] input:not([type=checkbox],[type=radio]):focus,[role=search] select:focus,[role=search]>:focus{z-index:2}[role=group] [role=button]:not(:first-child),[role=group] [type=button]:not(:first-child),[role=group] [type=reset]:not(:first-child),[role=group] [type=submit]:not(:first-child),[role=group] button:not(:first-child),[role=group] input:not([type=checkbox],[type=radio]):not(:first-child),[role=group] select:not(:first-child),[role=search] [role=button]:not(:first-child),[role=search] [type=button]:not(:first-child),[role=search] [type=reset]:not(:first-child),[role=search] [type=submit]:not(:first-child),[role=search] button:not(:first-child),[role=search] input:not([type=checkbox],[type=radio]):not(:first-child),[role=search] select:not(:first-child){margin-left:calc(var(--pico-border-width) * -1)}[role=group] [role=button],[role=group] [type=button],[role=group] [type=reset],[role=group] [type=submit],[role=group] button,[role=search] [role=button],[role=search] [type=button],[role=search] [type=reset],[role=search] [type=submit],[role=search] button{width:auto}@supports selector(:has(*)){[role=group]:has(button:focus,[type=submit]:focus,[type=button]:focus,[role=button]:focus),[role=search]:has(button:focus,[type=submit]:focus,[type=button]:focus,[role=button]:focus){--pico-group-box-shadow:var(--pico-group-box-shadow-focus-with-button)}[role=group]:has(button:focus,[type=submit]:focus,[type=button]:focus,[role=button]:focus) input:not([type=checkbox],[type=radio]),[role=group]:has(button:focus,[type=submit]:focus,[type=button]:focus,[role=button]:focus) select,[role=search]:has(button:focus,[type=submit]:focus,[type=button]:focus,[role=button]:focus) input:not([type=checkbox],[type=radio]),[role=search]:has(button:focus,[type=submit]:focus,[type=button]:focus,[role=button]:focus) select{border-color:transparent}[role=group]:has(input:not([type=submit],[type=button]):focus,select:focus),[role=search]:has(input:not([type=submit],[type=button]):focus,select:focus){--pico-group-box-shadow:var(--pico-group-box-shadow-focus-with-input)}[role=group]:has(input:not([type=submit],[type=button]):focus,select:focus) [role=button],[role=group]:has(input:not([type=submit],[type=button]):focus,select:focus) [type=button],[role=group]:has(input:not([type=submit],[type=button]):focus,select:focus) [type=submit],[role=group]:has(input:not([type=submit],[type=button]):focus,select:focus) button,[role=search]:has(input:not([type=submit],[type=button]):focus,select:focus) [role=button],[role=search]:has(input:not([type=submit],[type=button]):focus,select:focus) [type=button],[role=search]:has(input:not([type=submit],[type=button]):focus,select:focus) [type=submit],[role=search]:has(input:not([type=submit],[type=button]):focus,select:focus) button{--pico-button-box-shadow:0 0 0 var(--pico-border-width) var(--pico-primary-border);--pico-button-hover-box-shadow:0 0 0 var(--pico-border-width) var(--pico-primary-hover-border)}[role=group] [role=button]:focus,[role=group] [type=button]:focus,[role=group] [type=reset]:focus,[role=group] [type=submit]:focus,[role=group] button:focus,[role=search] [role=button]:focus,[role=search] [type=button]:focus,[role=search] [type=reset]:focus,[role=search] [type=submit]:focus,[role=search] button:focus{box-shadow:none}}[role=search]>:first-child{border-top-left-radius:5rem;border-bottom-left-radius:5rem}[role=search]>:last-child{border-top-right-radius:5rem;border-bottom-right-radius:5rem}[aria-busy=true]:not(input,select,textarea,html,form){white-space:nowrap}[aria-busy=true]:not(input,select,textarea,html,form)::before{display:inline-block;width:1em;height:1em;background-image:var(--pico-icon-loading);background-size:1em auto;background-repeat:no-repeat;content:"";vertical-align:-.125em}[aria-busy=true]:not(input,select,textarea,html,form):not(:empty)::before{margin-inline-end:calc(var(--pico-spacing) * .5)}[aria-busy=true]:not(input,select,textarea,html,form):empty{text-align:center}[role=button][aria-busy=true],[type=button][aria-busy=true],[type=reset][aria-busy=true],[type=submit][aria-busy=true],a[aria-busy=true],button[aria-busy=true]{pointer-events:none}:host,:root{--pico-scrollbar-width:0px}dialog{display:flex;z-index:999;position:fixed;top:0;right:0;bottom:0;left:0;align-items:center;justify-content:center;width:inherit;min-width:100%;height:inherit;min-height:100%;padding:0;border:0;-webkit-backdrop-filter:var(--pico-modal-overlay-backdrop-filter);backdrop-filter:var(--pico-modal-overlay-backdrop-filter);background-color:var(--pico-modal-overlay-background-color);color:var(--pico-color)}dialog>article{width:100%;max-height:calc(100vh - var(--pico-spacing) * 2);margin:var(--pico-spacing);overflow:auto}@media (min-width:576px){dialog>article{max-width:510px}}@media (min-width:768px){dialog>article{max-width:700px}}dialog>article>header>*{margin-bottom:0}dialog>article>header .close,dialog>article>header :is(a,button)[rel=prev]{margin:0;margin-left:var(--pico-spacing);padding:0;float:right}dialog>article>footer{text-align:right}dialog>article>footer [role=button],dialog>article>footer button{margin-bottom:0}dialog>article>footer [role=button]:not(:first-of-type),dialog>article>footer button:not(:first-of-type){margin-left:calc(var(--pico-spacing) * .5)}dialog>article .close,dialog>article :is(a,button)[rel=prev]{display:block;width:1rem;height:1rem;margin-top:calc(var(--pico-spacing) * -1);margin-bottom:var(--pico-spacing);margin-left:auto;border:none;background-image:var(--pico-icon-close);background-position:center;background-size:auto 1rem;background-repeat:no-repeat;background-color:transparent;opacity:.5;transition:opacity var(--pico-transition)}dialog>article .close:is([aria-current]:not([aria-current=false]),:hover,:active,:focus),dialog>article :is(a,button)[rel=prev]:is([aria-current]:not([aria-current=false]),:hover,:active,:focus){opacity:1}dialog:not([open]),dialog[open=false]{display:none}.modal-is-open{padding-right:var(--pico-scrollbar-width,0);overflow:hidden;pointer-events:none;touch-action:none}.modal-is-open dialog{pointer-events:auto;touch-action:auto}:where(.modal-is-opening,.modal-is-closing) dialog,:where(.modal-is-opening,.modal-is-closing) dialog>article{animation-duration:.2s;animation-timing-function:ease-in-out;animation-fill-mode:both}:where(.modal-is-opening,.modal-is-closing) dialog{animation-duration:.8s;animation-name:modal-overlay}:where(.modal-is-opening,.modal-is-closing) dialog>article{animation-delay:.2s;animation-name:modal}.modal-is-closing dialog,.modal-is-closing dialog>article{animation-delay:0s;animation-direction:reverse}@keyframes modal-overlay{from{-webkit-backdrop-filter:none;backdrop-filter:none;background-color:transparent}}@keyframes modal{from{transform:translateY(-100%);opacity:0}}:where(nav li)::before{float:left;content:""}nav,nav ul{display:flex}nav{justify-content:space-between;overflow:visible}nav ol,nav ul{align-items:center;margin-bottom:0;padding:0;list-style:none}nav ol:first-of-type,nav ul:first-of-type{margin-left:calc(var(--pico-nav-element-spacing-horizontal) * -1)}nav ol:last-of-type,nav ul:last-of-type{margin-right:calc(var(--pico-nav-element-spacing-horizontal) * -1)}nav li{display:inline-block;margin:0;padding:var(--pico-nav-element-spacing-vertical) var(--pico-nav-element-spacing-horizontal)}nav li :where(a,[role=link]){display:inline-block;margin:calc(var(--pico-nav-link-spacing-vertical) * -1) calc(var(--pico-nav-link-spacing-horizontal) * -1);padding:var(--pico-nav-link-spacing-vertical) var(--pico-nav-link-spacing-horizontal);border-radius:var(--pico-border-radius)}nav li :where(a,[role=link]):not(:hover){text-decoration:none}nav li [role=button],nav li [type=button],nav li button,nav li input:not([type=checkbox],[type=radio],[type=range],[type=file]),nav li select{height:auto;margin-right:inherit;margin-bottom:0;margin-left:inherit;padding:calc(var(--pico-nav-link-spacing-vertical) - var(--pico-border-width) * 2) var(--pico-nav-link-spacing-horizontal)}nav[aria-label=breadcrumb]{align-items:center;justify-content:start}nav[aria-label=breadcrumb] ul li:not(:first-child){margin-inline-start:var(--pico-nav-link-spacing-horizontal)}nav[aria-label=breadcrumb] ul li a{margin:calc(var(--pico-nav-link-spacing-vertical) * -1) 0;margin-inline-start:calc(var(--pico-nav-link-spacing-horizontal) * -1)}nav[aria-label=breadcrumb] ul li:not(:last-child)::after{display:inline-block;position:absolute;width:calc(var(--pico-nav-link-spacing-horizontal) * 4);margin:0 calc(var(--pico-nav-link-spacing-horizontal) * -1);content:var(--pico-nav-breadcrumb-divider);color:var(--pico-muted-color);text-align:center;text-decoration:none;white-space:nowrap}nav[aria-label=breadcrumb] a[aria-current]:not([aria-current=false]){background-color:transparent;color:inherit;text-decoration:none;pointer-events:none}aside li,aside nav,aside ol,aside ul{display:block}aside li{padding:calc(var(--pico-nav-element-spacing-vertical) * .5) var(--pico-nav-element-spacing-horizontal)}aside li a{display:block}aside li [role=button]{margin:inherit}[dir=rtl] nav[aria-label=breadcrumb] ul li:not(:last-child) ::after{content:"\\"}progress{display:inline-block;vertical-align:baseline}progress{-webkit-appearance:none;-moz-appearance:none;display:inline-block;appearance:none;width:100%;height:.5rem;margin-bottom:calc(var(--pico-spacing) * .5);overflow:hidden;border:0;border-radius:var(--pico-border-radius);background-color:var(--pico-progress-background-color);color:var(--pico-progress-color)}progress::-webkit-progress-bar{border-radius:var(--pico-border-radius);background:0 0}progress[value]::-webkit-progress-value{background-color:var(--pico-progress-color);-webkit-transition:inline-size var(--pico-transition);transition:inline-size var(--pico-transition)}progress::-moz-progress-bar{background-color:var(--pico-progress-color)}@media (prefers-reduced-motion:no-preference){progress:indeterminate{background:var(--pico-progress-background-color) linear-gradient(to right,var(--pico-progress-color) 30%,var(--pico-progress-background-color) 30%) top left/150% 150% no-repeat;animation:progress-indeterminate 1s linear infinite}progress:indeterminate[value]::-webkit-progress-value{background-color:transparent}progress:indeterminate::-moz-progress-bar{background-color:transparent}}@media (prefers-reduced-motion:no-preference){[dir=rtl] progress:indeterminate{animation-direction:reverse}}@keyframes progress-indeterminate{0%{background-position:200% 0}100%{background-position:-200% 0}}[data-tooltip]{position:relative}[data-tooltip]:not(a,button,input,[role=button]){border-bottom:1px dotted;text-decoration:none;cursor:help}[data-tooltip]::after,[data-tooltip]::before,[data-tooltip][data-placement=top]::after,[data-tooltip][data-placement=top]::before{display:block;z-index:99;position:absolute;bottom:100%;left:50%;padding:.25rem .5rem;overflow:hidden;transform:translate(-50%,-.25rem);border-radius:var(--pico-border-radius);background:var(--pico-tooltip-background-color);content:attr(data-tooltip);color:var(--pico-tooltip-color);font-style:normal;font-weight:var(--pico-font-weight);font-size:.875rem;text-decoration:none;text-overflow:ellipsis;white-space:nowrap;opacity:0;pointer-events:none}[data-tooltip]::after,[data-tooltip][data-placement=top]::after{padding:0;transform:translate(-50%,0);border-top:.3rem solid;border-right:.3rem solid transparent;border-left:.3rem solid transparent;border-radius:0;background-color:transparent;content:"";color:var(--pico-tooltip-background-color)}[data-tooltip][data-placement=bottom]::after,[data-tooltip][data-placement=bottom]::before{top:100%;bottom:auto;transform:translate(-50%,.25rem)}[data-tooltip][data-placement=bottom]:after{transform:translate(-50%,-.3rem);border:.3rem solid transparent;border-bottom:.3rem solid}[data-tooltip][data-placement=left]::after,[data-tooltip][data-placement=left]::before{top:50%;right:100%;bottom:auto;left:auto;transform:translate(-.25rem,-50%)}[data-tooltip][data-placement=left]:after{transform:translate(.3rem,-50%);border:.3rem solid transparent;border-left:.3rem solid}[data-tooltip][data-placement=right]::after,[data-tooltip][data-placement=right]::before{top:50%;right:auto;bottom:auto;left:100%;transform:translate(.25rem,-50%)}[data-tooltip][data-placement=right]:after{transform:translate(-.3rem,-50%);border:.3rem solid transparent;border-right:.3rem solid}[data-tooltip]:focus::after,[data-tooltip]:focus::before,[data-tooltip]:hover::after,[data-tooltip]:hover::before{opacity:1}@media (hover:hover) and (pointer:fine){[data-tooltip]:focus::after,[data-tooltip]:focus::before,[data-tooltip]:hover::after,[data-tooltip]:hover::before{--pico-tooltip-slide-to:translate(-50%, -0.25rem);transform:translate(-50%,.75rem);animation-duration:.2s;animation-fill-mode:forwards;animation-name:tooltip-slide;opacity:0}[data-tooltip]:focus::after,[data-tooltip]:hover::after{--pico-tooltip-caret-slide-to:translate(-50%, 0rem);transform:translate(-50%,-.25rem);animation-name:tooltip-caret-slide}[data-tooltip][data-placement=bottom]:focus::after,[data-tooltip][data-placement=bottom]:focus::before,[data-tooltip][data-placement=bottom]:hover::after,[data-tooltip][data-placement=bottom]:hover::before{--pico-tooltip-slide-to:translate(-50%, 0.25rem);transform:translate(-50%,-.75rem);animation-name:tooltip-slide}[data-tooltip][data-placement=bottom]:focus::after,[data-tooltip][data-placement=bottom]:hover::after{--pico-tooltip-caret-slide-to:translate(-50%, -0.3rem);transform:translate(-50%,-.5rem);animation-name:tooltip-caret-slide}[data-tooltip][data-placement=left]:focus::after,[data-tooltip][data-placement=left]:focus::before,[data-tooltip][data-placement=left]:hover::after,[data-tooltip][data-placement=left]:hover::before{--pico-tooltip-slide-to:translate(-0.25rem, -50%);transform:translate(.75rem,-50%);animation-name:tooltip-slide}[data-tooltip][data-placement=left]:focus::after,[data-tooltip][data-placement=left]:hover::after{--pico-tooltip-caret-slide-to:translate(0.3rem, -50%);transform:translate(.05rem,-50%);animation-name:tooltip-caret-slide}[data-tooltip][data-placement=right]:focus::after,[data-tooltip][data-placement=right]:focus::before,[data-tooltip][data-placement=right]:hover::after,[data-tooltip][data-placement=right]:hover::before{--pico-tooltip-slide-to:translate(0.25rem, -50%);transform:translate(-.75rem,-50%);animation-name:tooltip-slide}[data-tooltip][data-placement=right]:focus::after,[data-tooltip][data-placement=right]:hover::after{--pico-tooltip-caret-slide-to:translate(-0.3rem, -50%);transform:translate(-.05rem,-50%);animation-name:tooltip-caret-slide}}@keyframes tooltip-slide{to{transform:var(--pico-tooltip-slide-to);opacity:1}}@keyframes tooltip-caret-slide{50%{opacity:0}to{transform:var(--pico-tooltip-caret-slide-to);opacity:1}}[aria-controls]{cursor:pointer}[aria-disabled=true],[disabled]{cursor:not-allowed}[aria-hidden=false][hidden]{display:initial}[aria-hidden=false][hidden]:not(:focus){clip:rect(0,0,0,0);position:absolute}[tabindex],a,area,button,input,label,select,summary,textarea{-ms-touch-action:manipulation}[dir=rtl]{direction:rtl}@media (prefers-reduced-motion:reduce){:not([aria-busy=true]),:not([aria-busy=true])::after,:not([aria-busy=true])::before{background-attachment:initial!important;animation-duration:1ms!important;animation-delay:-1ms!important;animation-iteration-count:1!important;scroll-behavior:auto!important;transition-delay:0s!important;transition-duration:0s!important}} \ No newline at end of file diff --git a/server/static/style.css b/server/static/style.css deleted file mode 100644 index 57b9828..0000000 --- a/server/static/style.css +++ /dev/null @@ -1,83 +0,0 @@ -:root { - --zinc-700: rgb(66, 71, 81); - --success: rgb(0, 166, 110); - --danger: rgb(155, 35, 24); -} - -body { - display: flex; - flex-direction: column; -} - -main { -} - -.margin-top-sm { - margin-top: 2em; -} - -.margin-top-md { - margin-top: 2.5em; -} - -.margin-bottom-xs { - margin-bottom: 1.5em; -} - -.centered-body { - min-height: 100vh; - justify-content: center; -} - -.base-container { - border: 1px solid var(--zinc-700); - border-radius: 10px; - padding: 1.75em 1.2em; -} - -.box-shadow-container { - box-shadow: 1px 1px 52px 2px rgba(0, 0, 0, 0.42); -} - -.login-container { - max-width: 50ch; - form :last-child { - margin-bottom: 0; - } - form button { - float: right; - } -} - -.authorize-container { - max-width: 100ch; -} - -button { - width: unset; - min-width: 16ch; -} - -.button-row { - display: flex; - gap: 1ch; - justify-content: end; -} - -.alert { - border: 1px solid var(--zinc-700); - border-radius: 10px; - padding: 1em 1em; - p { - color: white; - margin-bottom: unset; - } -} - -.alert-success { - background-color: var(--success); -} - -.alert-danger { - background-color: var(--danger); -} diff --git a/server/templates/account.html b/server/templates/account.html deleted file mode 100644 index 6035679..0000000 --- a/server/templates/account.html +++ /dev/null @@ -1,40 +0,0 @@ - - -
- - - - - -{{ index .flashes.successes 0 }}
-Session Age: {{ .Age}}
-Last Updated: {{ .LastUpdated }} ago
-Expires In: {{ .ExpiresIn }}
-IP Address: {{ .Ip }}
- -- You are signed in as {{ .Handle }}. - Switch Account -
-{{ .AppName }} is asking for you to grant it these scopes:
-- If you press Accept, the application will be granted permissions for - these scopes with your account {{ .Handle }}. If you reject, you - will be sent back to the application. -
- -Enter your handle and password below.
- {{ if .flashes.errors }} -{{ index .flashes.errors 0 }}
-