An atproto PDS written in Go

Compare changes

Choose any two refs to compare.

Changed files
+2063 -599
.github
workflows
cmd
cocoon
internal
db
metrics
models
oauth
server
templates
sqlite_blockstore
+63 -7
.github/workflows/docker-image.yml
··· 5 push: 6 branches: 7 - main 8 9 env: 10 REGISTRY: ghcr.io ··· 12 13 jobs: 14 build-and-push-image: 15 - runs-on: ubuntu-latest 16 # Sets the permissions granted to the `GITHUB_TOKEN` for the actions in this job. 17 permissions: 18 contents: read 19 packages: write 20 attestations: write 21 id-token: write 22 - # 23 steps: 24 - name: Checkout repository 25 uses: actions/checkout@v4 26 # Uses the `docker/login-action` action to log in to the Container registry registry using the account and password that will publish the packages. Once published, the packages are scoped to the account defined here. 27 - name: Log in to the Container registry 28 - uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1 29 with: 30 registry: ${{ env.REGISTRY }} 31 username: ${{ github.actor }} 32 password: ${{ secrets.GITHUB_TOKEN }} 33 # This step uses [docker/metadata-action](https://github.com/docker/metadata-action#about) to extract tags and labels that will be applied to the specified image. The `id` "meta" allows the output of this step to be referenced in a subsequent step. The `images` value provides the base name for the tags and labels. 34 - name: Extract metadata (tags, labels) for Docker 35 id: meta ··· 37 with: 38 images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} 39 tags: | 40 - type=sha 41 - type=sha,format=long 42 # This step uses the `docker/build-push-action` action to build the image, based on your repository's `Dockerfile`. If the build succeeds, it pushes the image to GitHub Packages. 43 # It uses the `context` parameter to define the build's context as the set of files located in the specified path. For more information, see "[Usage](https://github.com/docker/build-push-action#usage)" in the README of the `docker/build-push-action` repository. 44 # It uses the `tags` and `labels` parameters to tag and label the image with the output from the "meta" step. 45 - name: Build and push Docker image 46 id: push 47 - uses: docker/build-push-action@v5 48 with: 49 context: . 50 push: true 51 tags: ${{ steps.meta.outputs.tags }} 52 labels: ${{ steps.meta.outputs.labels }} 53 54 # This step generates an artifact attestation for the image, which is an unforgeable statement about where and how it was built. It increases supply chain security for people who consume the image. For more information, see "[AUTOTITLE](/actions/security-guides/using-artifact-attestations-to-establish-provenance-for-builds)." 55 - name: Generate artifact attestation 56 uses: actions/attest-build-provenance@v1 57 with: 58 subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME}} 59 - subject-digest: ${{ steps.push.outputs.digest }} 60 push-to-registry: true
··· 5 push: 6 branches: 7 - main 8 + tags: 9 + - 'v*' 10 11 env: 12 REGISTRY: ghcr.io ··· 14 15 jobs: 16 build-and-push-image: 17 + strategy: 18 + matrix: 19 + include: 20 + - arch: amd64 21 + runner: ubuntu-latest 22 + - arch: arm64 23 + runner: ubuntu-24.04-arm 24 + runs-on: ${{ matrix.runner }} 25 # Sets the permissions granted to the `GITHUB_TOKEN` for the actions in this job. 26 permissions: 27 contents: read 28 packages: write 29 attestations: write 30 id-token: write 31 + outputs: 32 + digest-amd64: ${{ matrix.arch == 'amd64' && steps.push.outputs.digest || '' }} 33 + digest-arm64: ${{ matrix.arch == 'arm64' && steps.push.outputs.digest || '' }} 34 steps: 35 - name: Checkout repository 36 uses: actions/checkout@v4 37 + 38 # Uses the `docker/login-action` action to log in to the Container registry registry using the account and password that will publish the packages. Once published, the packages are scoped to the account defined here. 39 - name: Log in to the Container registry 40 + uses: docker/login-action@v3 41 with: 42 registry: ${{ env.REGISTRY }} 43 username: ${{ github.actor }} 44 password: ${{ secrets.GITHUB_TOKEN }} 45 + 46 # This step uses [docker/metadata-action](https://github.com/docker/metadata-action#about) to extract tags and labels that will be applied to the specified image. The `id` "meta" allows the output of this step to be referenced in a subsequent step. The `images` value provides the base name for the tags and labels. 47 - name: Extract metadata (tags, labels) for Docker 48 id: meta ··· 50 with: 51 images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} 52 tags: | 53 + type=raw,value=latest,enable={{is_default_branch}},suffix=-${{ matrix.arch }} 54 + type=sha,suffix=-${{ matrix.arch }} 55 + type=sha,format=long,suffix=-${{ matrix.arch }} 56 + type=semver,pattern={{version}},suffix=-${{ matrix.arch }} 57 + type=semver,pattern={{major}}.{{minor}},suffix=-${{ matrix.arch }} 58 + 59 # This step uses the `docker/build-push-action` action to build the image, based on your repository's `Dockerfile`. If the build succeeds, it pushes the image to GitHub Packages. 60 # It uses the `context` parameter to define the build's context as the set of files located in the specified path. For more information, see "[Usage](https://github.com/docker/build-push-action#usage)" in the README of the `docker/build-push-action` repository. 61 # It uses the `tags` and `labels` parameters to tag and label the image with the output from the "meta" step. 62 - name: Build and push Docker image 63 id: push 64 + uses: docker/build-push-action@v6 65 with: 66 context: . 67 push: true 68 tags: ${{ steps.meta.outputs.tags }} 69 labels: ${{ steps.meta.outputs.labels }} 70 71 + publish-manifest: 72 + needs: build-and-push-image 73 + runs-on: ubuntu-latest 74 + permissions: 75 + packages: write 76 + attestations: write 77 + id-token: write 78 + steps: 79 + - name: Log in to the Container registry 80 + uses: docker/login-action@v3 81 + with: 82 + registry: ${{ env.REGISTRY }} 83 + username: ${{ github.actor }} 84 + password: ${{ secrets.GITHUB_TOKEN }} 85 + 86 + - name: Extract metadata (tags, labels) for Docker 87 + id: meta 88 + uses: docker/metadata-action@v5 89 + with: 90 + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} 91 + tags: | 92 + type=raw,value=latest,enable={{is_default_branch}} 93 + type=sha 94 + type=sha,format=long 95 + type=semver,pattern={{version}} 96 + type=semver,pattern={{major}}.{{minor}} 97 + 98 + - name: Create and push manifest 99 + run: | 100 + # Split tags into an array 101 + readarray -t tags <<< "${{ steps.meta.outputs.tags }}" 102 + 103 + # Create and push manifest for each tag 104 + for tag in "${tags[@]}"; do 105 + docker buildx imagetools create -t "$tag" \ 106 + "${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@${{ needs.build-and-push-image.outputs.digest-amd64 }}" \ 107 + "${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@${{ needs.build-and-push-image.outputs.digest-arm64 }}" 108 + done 109 + 110 # This step generates an artifact attestation for the image, which is an unforgeable statement about where and how it was built. It increases supply chain security for people who consume the image. For more information, see "[AUTOTITLE](/actions/security-guides/using-artifact-attestations-to-establish-provenance-for-builds)." 111 - name: Generate artifact attestation 112 uses: actions/attest-build-provenance@v1 113 with: 114 subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME}} 115 + subject-digest: ${{ needs.build-and-push-image.outputs.digest-amd64 }} 116 push-to-registry: true
+1
.gitignore
··· 6 .DS_Store 7 data/ 8 keys/
··· 6 .DS_Store 7 data/ 8 keys/ 9 + dist/
+10
Caddyfile.postgres
···
··· 1 + {$COCOON_HOSTNAME} { 2 + reverse_proxy cocoon:8080 3 + 4 + encode gzip 5 + 6 + log { 7 + output file /data/access.log 8 + format json 9 + } 10 + }
+1 -1
Dockerfile
··· 11 ### Run stage 12 FROM debian:bookworm-slim AS run 13 14 - RUN apt-get update && apt-get install -y dumb-init runit ca-certificates && rm -rf /var/lib/apt/lists/* 15 ENTRYPOINT ["dumb-init", "--"] 16 17 WORKDIR /
··· 11 ### Run stage 12 FROM debian:bookworm-slim AS run 13 14 + RUN apt-get update && apt-get install -y dumb-init runit ca-certificates curl && rm -rf /var/lib/apt/lists/* 15 ENTRYPOINT ["dumb-init", "--"] 16 17 WORKDIR /
+37 -1
Makefile
··· 4 GIT_COMMIT := $(shell git rev-parse --short=9 HEAD) 5 VERSION := $(if $(GIT_TAG),$(GIT_TAG),dev-$(GIT_COMMIT)) 6 7 .PHONY: help 8 help: ## Print info about all commands 9 @echo "Commands:" ··· 14 build: ## Build all executables 15 go build -ldflags "-X main.Version=$(VERSION)" -o cocoon ./cmd/cocoon 16 17 .PHONY: run 18 run: 19 go build -ldflags "-X main.Version=dev-local" -o cocoon ./cmd/cocoon && ./cocoon run ··· 43 44 .PHONY: docker-build 45 docker-build: 46 - docker build -t cocoon .
··· 4 GIT_COMMIT := $(shell git rev-parse --short=9 HEAD) 5 VERSION := $(if $(GIT_TAG),$(GIT_TAG),dev-$(GIT_COMMIT)) 6 7 + # Build output directory 8 + BUILD_DIR := dist 9 + 10 + # Platforms to build for 11 + PLATFORMS := \ 12 + linux/amd64 \ 13 + linux/arm64 \ 14 + linux/arm \ 15 + darwin/amd64 \ 16 + darwin/arm64 \ 17 + windows/amd64 \ 18 + windows/arm64 \ 19 + freebsd/amd64 \ 20 + freebsd/arm64 \ 21 + openbsd/amd64 \ 22 + openbsd/arm64 23 + 24 .PHONY: help 25 help: ## Print info about all commands 26 @echo "Commands:" ··· 31 build: ## Build all executables 32 go build -ldflags "-X main.Version=$(VERSION)" -o cocoon ./cmd/cocoon 33 34 + .PHONY: build-release 35 + build-all: ## Build binaries for all architectures 36 + @echo "Building for all architectures..." 37 + @mkdir -p $(BUILD_DIR) 38 + @$(foreach platform,$(PLATFORMS), \ 39 + $(eval OS := $(word 1,$(subst /, ,$(platform)))) \ 40 + $(eval ARCH := $(word 2,$(subst /, ,$(platform)))) \ 41 + $(eval EXT := $(if $(filter windows,$(OS)),.exe,)) \ 42 + $(eval OUTPUT := $(BUILD_DIR)/cocoon-$(VERSION)-$(OS)-$(ARCH)$(EXT)) \ 43 + echo "Building $(OS)/$(ARCH)..."; \ 44 + GOOS=$(OS) GOARCH=$(ARCH) go build -ldflags "-X main.Version=$(VERSION)" -o $(OUTPUT) ./cmd/cocoon && \ 45 + echo " โœ“ $(OUTPUT)" || echo " โœ— Failed: $(OS)/$(ARCH)"; \ 46 + ) 47 + @echo "Done! Binaries are in $(BUILD_DIR)/" 48 + 49 + .PHONY: clean-dist 50 + clean-dist: ## Remove all built binaries 51 + rm -rf $(BUILD_DIR) 52 + 53 .PHONY: run 54 run: 55 go build -ldflags "-X main.Version=dev-local" -o cocoon ./cmd/cocoon && ./cocoon run ··· 79 80 .PHONY: docker-build 81 docker-build: 82 + docker build -t cocoon .
+59 -6
README.md
··· 55 docker-compose up -d 56 ``` 57 58 5. **Get your invite code** 59 60 On first run, an invite code is automatically created. View it with: ··· 96 97 ### Optional Configuration 98 99 #### SMTP Email Settings 100 ```bash 101 COCOON_SMTP_USER="your-smtp-username" ··· 107 ``` 108 109 #### S3 Storage 110 ```bash 111 COCOON_S3_BACKUPS_ENABLED=true 112 COCOON_S3_BLOBSTORE_ENABLED=true 113 COCOON_S3_REGION="us-east-1" 114 COCOON_S3_BUCKET="your-bucket" 115 COCOON_S3_ENDPOINT="https://s3.amazonaws.com" 116 COCOON_S3_ACCESS_KEY="your-access-key" 117 COCOON_S3_SECRET_KEY="your-secret-key" 118 ``` 119 120 ### Management Commands 121 122 Create an invite code: ··· 160 - [x] `com.atproto.repo.getRecord` 161 - [x] `com.atproto.repo.importRepo` (Works "okay". Use with extreme caution.) 162 - [x] `com.atproto.repo.listRecords` 163 - - [ ] `com.atproto.repo.listMissingBlobs` 164 165 ### Server 166 ··· 171 - [x] `com.atproto.server.createInviteCode` 172 - [x] `com.atproto.server.createInviteCodes` 173 - [x] `com.atproto.server.deactivateAccount` 174 - - [ ] `com.atproto.server.deleteAccount` 175 - [x] `com.atproto.server.deleteSession` 176 - [x] `com.atproto.server.describeServer` 177 - [ ] `com.atproto.server.getAccountInviteCodes` 178 - - [ ] `com.atproto.server.getServiceAuth` 179 - ~~[ ] `com.atproto.server.listAppPasswords`~~ - not going to add app passwords 180 - [x] `com.atproto.server.refreshSession` 181 - - [ ] `com.atproto.server.requestAccountDelete` 182 - [x] `com.atproto.server.requestEmailConfirmation` 183 - [x] `com.atproto.server.requestEmailUpdate` 184 - [x] `com.atproto.server.requestPasswordReset` 185 - - [ ] `com.atproto.server.reserveSigningKey` 186 - [x] `com.atproto.server.resetPassword` 187 - ~~[] `com.atproto.server.revokeAppPassword`~~ - not going to add app passwords 188 - [x] `com.atproto.server.updateEmail` ··· 203 204 ### Other 205 206 - - [ ] `com.atproto.label.queryLabels` 207 - [x] `com.atproto.moderation.createReport` (Note: this should be handled by proxying, not actually implemented in the PDS) 208 - [x] `app.bsky.actor.getPreferences` 209 - [x] `app.bsky.actor.putPreferences`
··· 55 docker-compose up -d 56 ``` 57 58 + **For PostgreSQL deployment:** 59 + ```bash 60 + # Add POSTGRES_PASSWORD to your .env file first! 61 + docker-compose -f docker-compose.postgres.yaml up -d 62 + ``` 63 + 64 5. **Get your invite code** 65 66 On first run, an invite code is automatically created. View it with: ··· 102 103 ### Optional Configuration 104 105 + #### Database Configuration 106 + 107 + By default, Cocoon uses SQLite which requires no additional setup. For production deployments with higher traffic, you can use PostgreSQL: 108 + 109 + ```bash 110 + # Database type: sqlite (default) or postgres 111 + COCOON_DB_TYPE="postgres" 112 + 113 + # PostgreSQL connection string (required if db-type is postgres) 114 + # Format: postgres://user:password@host:port/database?sslmode=disable 115 + COCOON_DATABASE_URL="postgres://cocoon:password@localhost:5432/cocoon?sslmode=disable" 116 + 117 + # Or use the standard DATABASE_URL environment variable 118 + DATABASE_URL="postgres://cocoon:password@localhost:5432/cocoon?sslmode=disable" 119 + ``` 120 + 121 + For SQLite (default): 122 + ```bash 123 + COCOON_DB_TYPE="sqlite" 124 + COCOON_DB_NAME="/data/cocoon/cocoon.db" 125 + ``` 126 + 127 + > **Note**: When using PostgreSQL, database backups to S3 are not handled by Cocoon. Use `pg_dump` or your database provider's backup solution instead. 128 + 129 #### SMTP Email Settings 130 ```bash 131 COCOON_SMTP_USER="your-smtp-username" ··· 137 ``` 138 139 #### S3 Storage 140 + 141 + Cocoon supports S3-compatible storage for both database backups (SQLite only) and blob storage (images, videos, etc.): 142 + 143 ```bash 144 + # Enable S3 backups (SQLite databases only - hourly backups) 145 COCOON_S3_BACKUPS_ENABLED=true 146 + 147 + # Enable S3 for blob storage (images, videos, etc.) 148 + # When enabled, blobs are stored in S3 instead of the database 149 COCOON_S3_BLOBSTORE_ENABLED=true 150 + 151 + # S3 configuration (works with AWS S3, MinIO, Cloudflare R2, etc.) 152 COCOON_S3_REGION="us-east-1" 153 COCOON_S3_BUCKET="your-bucket" 154 COCOON_S3_ENDPOINT="https://s3.amazonaws.com" 155 COCOON_S3_ACCESS_KEY="your-access-key" 156 COCOON_S3_SECRET_KEY="your-secret-key" 157 + 158 + # Optional: CDN/public URL for blob redirects 159 + # When set, com.atproto.sync.getBlob redirects to this URL instead of proxying 160 + COCOON_S3_CDN_URL="https://cdn.example.com" 161 ``` 162 163 + **Blob Storage Options:** 164 + - `COCOON_S3_BLOBSTORE_ENABLED=false` (default): Blobs stored in the database 165 + - `COCOON_S3_BLOBSTORE_ENABLED=true`: Blobs stored in S3 bucket under `blobs/{did}/{cid}` 166 + 167 + **Blob Serving Options:** 168 + - Without `COCOON_S3_CDN_URL`: Blobs are proxied through the PDS server 169 + - With `COCOON_S3_CDN_URL`: `getBlob` returns a 302 redirect to `{CDN_URL}/blobs/{did}/{cid}` 170 + 171 + > **Tip**: For Cloudflare R2, you can use the public bucket URL as the CDN URL. For AWS S3, you can use CloudFront or the S3 bucket URL directly if public access is enabled. 172 + 173 ### Management Commands 174 175 Create an invite code: ··· 213 - [x] `com.atproto.repo.getRecord` 214 - [x] `com.atproto.repo.importRepo` (Works "okay". Use with extreme caution.) 215 - [x] `com.atproto.repo.listRecords` 216 + - [x] `com.atproto.repo.listMissingBlobs` 217 218 ### Server 219 ··· 224 - [x] `com.atproto.server.createInviteCode` 225 - [x] `com.atproto.server.createInviteCodes` 226 - [x] `com.atproto.server.deactivateAccount` 227 + - [x] `com.atproto.server.deleteAccount` 228 - [x] `com.atproto.server.deleteSession` 229 - [x] `com.atproto.server.describeServer` 230 - [ ] `com.atproto.server.getAccountInviteCodes` 231 + - [x] `com.atproto.server.getServiceAuth` 232 - ~~[ ] `com.atproto.server.listAppPasswords`~~ - not going to add app passwords 233 - [x] `com.atproto.server.refreshSession` 234 + - [x] `com.atproto.server.requestAccountDelete` 235 - [x] `com.atproto.server.requestEmailConfirmation` 236 - [x] `com.atproto.server.requestEmailUpdate` 237 - [x] `com.atproto.server.requestPasswordReset` 238 + - [x] `com.atproto.server.reserveSigningKey` 239 - [x] `com.atproto.server.resetPassword` 240 - ~~[] `com.atproto.server.revokeAppPassword`~~ - not going to add app passwords 241 - [x] `com.atproto.server.updateEmail` ··· 256 257 ### Other 258 259 + - [x] `com.atproto.label.queryLabels` 260 - [x] `com.atproto.moderation.createReport` (Note: this should be handled by proxying, not actually implemented in the PDS) 261 - [x] `app.bsky.actor.getPreferences` 262 - [x] `app.bsky.actor.putPreferences`
+59 -4
cmd/cocoon/main.go
··· 9 "os" 10 "time" 11 12 "github.com/bluesky-social/indigo/atproto/atcrypto" 13 "github.com/bluesky-social/indigo/atproto/syntax" 14 "github.com/haileyok/cocoon/internal/helpers" ··· 17 "github.com/lestrrat-go/jwx/v2/jwk" 18 "github.com/urfave/cli/v2" 19 "golang.org/x/crypto/bcrypt" 20 "gorm.io/driver/sqlite" 21 "gorm.io/gorm" 22 ) ··· 39 EnvVars: []string{"COCOON_DB_NAME"}, 40 }, 41 &cli.StringFlag{ 42 Name: "did", 43 EnvVars: []string{"COCOON_DID"}, 44 }, ··· 66 Name: "admin-password", 67 EnvVars: []string{"COCOON_ADMIN_PASSWORD"}, 68 }, 69 &cli.StringFlag{ 70 Name: "smtp-user", 71 EnvVars: []string{"COCOON_SMTP_USER"}, ··· 117 &cli.StringFlag{ 118 Name: "s3-secret-key", 119 EnvVars: []string{"COCOON_S3_SECRET_KEY"}, 120 }, 121 &cli.StringFlag{ 122 Name: "session-secret", ··· 131 Name: "fallback-proxy", 132 EnvVars: []string{"COCOON_FALLBACK_PROXY"}, 133 }, 134 }, 135 Commands: []*cli.Command{ 136 runServe, ··· 154 Flags: []cli.Flag{}, 155 Action: func(cmd *cli.Context) error { 156 157 s, err := server.New(&server.Args{ 158 Addr: cmd.String("addr"), 159 DbName: cmd.String("db-name"), 160 Did: cmd.String("did"), 161 Hostname: cmd.String("hostname"), 162 RotationKeyPath: cmd.String("rotation-key-path"), ··· 165 Version: Version, 166 Relays: cmd.StringSlice("relays"), 167 AdminPassword: cmd.String("admin-password"), 168 SmtpUser: cmd.String("smtp-user"), 169 SmtpPass: cmd.String("smtp-pass"), 170 SmtpHost: cmd.String("smtp-host"), ··· 179 Endpoint: cmd.String("s3-endpoint"), 180 AccessKey: cmd.String("s3-access-key"), 181 SecretKey: cmd.String("s3-secret-key"), 182 }, 183 SessionSecret: cmd.String("session-secret"), 184 BlockstoreVariant: server.MustReturnBlockstoreVariant(cmd.String("blockstore-variant")), ··· 279 }, 280 }, 281 Action: func(cmd *cli.Context) error { 282 - db, err := newDb() 283 if err != nil { 284 return err 285 } ··· 318 }, 319 }, 320 Action: func(cmd *cli.Context) error { 321 - db, err := newDb() 322 if err != nil { 323 return err 324 } ··· 345 }, 346 } 347 348 - func newDb() (*gorm.DB, error) { 349 - return gorm.Open(sqlite.Open("cocoon.db"), &gorm.Config{}) 350 }
··· 9 "os" 10 "time" 11 12 + "github.com/bluesky-social/go-util/pkg/telemetry" 13 "github.com/bluesky-social/indigo/atproto/atcrypto" 14 "github.com/bluesky-social/indigo/atproto/syntax" 15 "github.com/haileyok/cocoon/internal/helpers" ··· 18 "github.com/lestrrat-go/jwx/v2/jwk" 19 "github.com/urfave/cli/v2" 20 "golang.org/x/crypto/bcrypt" 21 + "gorm.io/driver/postgres" 22 "gorm.io/driver/sqlite" 23 "gorm.io/gorm" 24 ) ··· 41 EnvVars: []string{"COCOON_DB_NAME"}, 42 }, 43 &cli.StringFlag{ 44 + Name: "db-type", 45 + Value: "sqlite", 46 + Usage: "Database type: sqlite or postgres", 47 + EnvVars: []string{"COCOON_DB_TYPE"}, 48 + }, 49 + &cli.StringFlag{ 50 + Name: "database-url", 51 + Aliases: []string{"db-url"}, 52 + Usage: "PostgreSQL connection string (required if db-type is postgres)", 53 + EnvVars: []string{"COCOON_DATABASE_URL", "DATABASE_URL"}, 54 + }, 55 + &cli.StringFlag{ 56 Name: "did", 57 EnvVars: []string{"COCOON_DID"}, 58 }, ··· 80 Name: "admin-password", 81 EnvVars: []string{"COCOON_ADMIN_PASSWORD"}, 82 }, 83 + &cli.BoolFlag{ 84 + Name: "require-invite", 85 + EnvVars: []string{"COCOON_REQUIRE_INVITE"}, 86 + Value: true, 87 + }, 88 &cli.StringFlag{ 89 Name: "smtp-user", 90 EnvVars: []string{"COCOON_SMTP_USER"}, ··· 136 &cli.StringFlag{ 137 Name: "s3-secret-key", 138 EnvVars: []string{"COCOON_S3_SECRET_KEY"}, 139 + }, 140 + &cli.StringFlag{ 141 + Name: "s3-cdn-url", 142 + EnvVars: []string{"COCOON_S3_CDN_URL"}, 143 + Usage: "Public URL for S3 blob redirects (e.g., https://cdn.example.com). When set, getBlob redirects to this URL instead of proxying.", 144 }, 145 &cli.StringFlag{ 146 Name: "session-secret", ··· 155 Name: "fallback-proxy", 156 EnvVars: []string{"COCOON_FALLBACK_PROXY"}, 157 }, 158 + telemetry.CLIFlagDebug, 159 + telemetry.CLIFlagMetricsListenAddress, 160 }, 161 Commands: []*cli.Command{ 162 runServe, ··· 180 Flags: []cli.Flag{}, 181 Action: func(cmd *cli.Context) error { 182 183 + logger := telemetry.StartLogger(cmd) 184 + telemetry.StartMetrics(cmd) 185 + 186 s, err := server.New(&server.Args{ 187 + Logger: logger, 188 Addr: cmd.String("addr"), 189 DbName: cmd.String("db-name"), 190 + DbType: cmd.String("db-type"), 191 + DatabaseURL: cmd.String("database-url"), 192 Did: cmd.String("did"), 193 Hostname: cmd.String("hostname"), 194 RotationKeyPath: cmd.String("rotation-key-path"), ··· 197 Version: Version, 198 Relays: cmd.StringSlice("relays"), 199 AdminPassword: cmd.String("admin-password"), 200 + RequireInvite: cmd.Bool("require-invite"), 201 SmtpUser: cmd.String("smtp-user"), 202 SmtpPass: cmd.String("smtp-pass"), 203 SmtpHost: cmd.String("smtp-host"), ··· 212 Endpoint: cmd.String("s3-endpoint"), 213 AccessKey: cmd.String("s3-access-key"), 214 SecretKey: cmd.String("s3-secret-key"), 215 + CDNUrl: cmd.String("s3-cdn-url"), 216 }, 217 SessionSecret: cmd.String("session-secret"), 218 BlockstoreVariant: server.MustReturnBlockstoreVariant(cmd.String("blockstore-variant")), ··· 313 }, 314 }, 315 Action: func(cmd *cli.Context) error { 316 + db, err := newDb(cmd) 317 if err != nil { 318 return err 319 } ··· 352 }, 353 }, 354 Action: func(cmd *cli.Context) error { 355 + db, err := newDb(cmd) 356 if err != nil { 357 return err 358 } ··· 379 }, 380 } 381 382 + func newDb(cmd *cli.Context) (*gorm.DB, error) { 383 + dbType := cmd.String("db-type") 384 + if dbType == "" { 385 + dbType = "sqlite" 386 + } 387 + 388 + switch dbType { 389 + case "postgres": 390 + databaseURL := cmd.String("database-url") 391 + if databaseURL == "" { 392 + databaseURL = cmd.String("database-url") 393 + } 394 + if databaseURL == "" { 395 + return nil, fmt.Errorf("COCOON_DATABASE_URL or DATABASE_URL must be set when using postgres") 396 + } 397 + return gorm.Open(postgres.Open(databaseURL), &gorm.Config{}) 398 + default: 399 + dbName := cmd.String("db-name") 400 + if dbName == "" { 401 + dbName = "cocoon.db" 402 + } 403 + return gorm.Open(sqlite.Open(dbName), &gorm.Config{}) 404 + } 405 }
+158
docker-compose.postgres.yaml
···
··· 1 + # Docker Compose with PostgreSQL 2 + # 3 + # Usage: 4 + # docker-compose -f docker-compose.postgres.yaml up -d 5 + # 6 + # This file extends the base docker-compose.yaml with a PostgreSQL database. 7 + # Set the following in your .env file: 8 + # COCOON_DB_TYPE=postgres 9 + # POSTGRES_PASSWORD=your-secure-password 10 + 11 + version: '3.8' 12 + 13 + services: 14 + postgres: 15 + image: postgres:16-alpine 16 + container_name: cocoon-postgres 17 + environment: 18 + POSTGRES_USER: cocoon 19 + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?POSTGRES_PASSWORD is required} 20 + POSTGRES_DB: cocoon 21 + volumes: 22 + - postgres_data:/var/lib/postgresql/data 23 + healthcheck: 24 + test: ["CMD-SHELL", "pg_isready -U cocoon -d cocoon"] 25 + interval: 10s 26 + timeout: 5s 27 + retries: 5 28 + restart: unless-stopped 29 + 30 + init-keys: 31 + build: 32 + context: . 33 + dockerfile: Dockerfile 34 + image: ghcr.io/haileyok/cocoon:latest 35 + container_name: cocoon-init-keys 36 + volumes: 37 + - ./keys:/keys 38 + - ./data:/data/cocoon 39 + - ./init-keys.sh:/init-keys.sh:ro 40 + environment: 41 + COCOON_DID: ${COCOON_DID} 42 + COCOON_HOSTNAME: ${COCOON_HOSTNAME} 43 + COCOON_ROTATION_KEY_PATH: /keys/rotation.key 44 + COCOON_JWK_PATH: /keys/jwk.key 45 + COCOON_CONTACT_EMAIL: ${COCOON_CONTACT_EMAIL} 46 + COCOON_RELAYS: ${COCOON_RELAYS:-https://bsky.network} 47 + COCOON_ADMIN_PASSWORD: ${COCOON_ADMIN_PASSWORD} 48 + entrypoint: ["/bin/sh", "/init-keys.sh"] 49 + restart: "no" 50 + 51 + cocoon: 52 + build: 53 + context: . 54 + dockerfile: Dockerfile 55 + image: ghcr.io/haileyok/cocoon:latest 56 + container_name: cocoon-pds 57 + depends_on: 58 + init-keys: 59 + condition: service_completed_successfully 60 + postgres: 61 + condition: service_healthy 62 + ports: 63 + - "8080:8080" 64 + volumes: 65 + - ./data:/data/cocoon 66 + - ./keys/rotation.key:/keys/rotation.key:ro 67 + - ./keys/jwk.key:/keys/jwk.key:ro 68 + environment: 69 + # Required settings 70 + COCOON_DID: ${COCOON_DID} 71 + COCOON_HOSTNAME: ${COCOON_HOSTNAME} 72 + COCOON_ROTATION_KEY_PATH: /keys/rotation.key 73 + COCOON_JWK_PATH: /keys/jwk.key 74 + COCOON_CONTACT_EMAIL: ${COCOON_CONTACT_EMAIL} 75 + COCOON_RELAYS: ${COCOON_RELAYS:-https://bsky.network} 76 + COCOON_ADMIN_PASSWORD: ${COCOON_ADMIN_PASSWORD} 77 + COCOON_SESSION_SECRET: ${COCOON_SESSION_SECRET} 78 + 79 + # Database configuration - PostgreSQL 80 + COCOON_ADDR: ":8080" 81 + COCOON_DB_TYPE: postgres 82 + COCOON_DATABASE_URL: postgres://cocoon:${POSTGRES_PASSWORD}@postgres:5432/cocoon?sslmode=disable 83 + COCOON_BLOCKSTORE_VARIANT: ${COCOON_BLOCKSTORE_VARIANT:-sqlite} 84 + 85 + # Optional: SMTP settings for email 86 + COCOON_SMTP_USER: ${COCOON_SMTP_USER:-} 87 + COCOON_SMTP_PASS: ${COCOON_SMTP_PASS:-} 88 + COCOON_SMTP_HOST: ${COCOON_SMTP_HOST:-} 89 + COCOON_SMTP_PORT: ${COCOON_SMTP_PORT:-} 90 + COCOON_SMTP_EMAIL: ${COCOON_SMTP_EMAIL:-} 91 + COCOON_SMTP_NAME: ${COCOON_SMTP_NAME:-} 92 + 93 + # Optional: S3 configuration 94 + COCOON_S3_BACKUPS_ENABLED: ${COCOON_S3_BACKUPS_ENABLED:-false} 95 + COCOON_S3_BLOBSTORE_ENABLED: ${COCOON_S3_BLOBSTORE_ENABLED:-false} 96 + COCOON_S3_REGION: ${COCOON_S3_REGION:-} 97 + COCOON_S3_BUCKET: ${COCOON_S3_BUCKET:-} 98 + COCOON_S3_ENDPOINT: ${COCOON_S3_ENDPOINT:-} 99 + COCOON_S3_ACCESS_KEY: ${COCOON_S3_ACCESS_KEY:-} 100 + COCOON_S3_SECRET_KEY: ${COCOON_S3_SECRET_KEY:-} 101 + 102 + # Optional: Fallback proxy 103 + COCOON_FALLBACK_PROXY: ${COCOON_FALLBACK_PROXY:-} 104 + restart: unless-stopped 105 + healthcheck: 106 + test: ["CMD", "curl", "-f", "http://localhost:8080/xrpc/_health"] 107 + interval: 30s 108 + timeout: 10s 109 + retries: 3 110 + start_period: 40s 111 + 112 + create-invite: 113 + build: 114 + context: . 115 + dockerfile: Dockerfile 116 + image: ghcr.io/haileyok/cocoon:latest 117 + container_name: cocoon-create-invite 118 + volumes: 119 + - ./keys:/keys 120 + - ./create-initial-invite.sh:/create-initial-invite.sh:ro 121 + environment: 122 + COCOON_DID: ${COCOON_DID} 123 + COCOON_HOSTNAME: ${COCOON_HOSTNAME} 124 + COCOON_ROTATION_KEY_PATH: /keys/rotation.key 125 + COCOON_JWK_PATH: /keys/jwk.key 126 + COCOON_CONTACT_EMAIL: ${COCOON_CONTACT_EMAIL} 127 + COCOON_RELAYS: ${COCOON_RELAYS:-https://bsky.network} 128 + COCOON_ADMIN_PASSWORD: ${COCOON_ADMIN_PASSWORD} 129 + COCOON_DB_TYPE: postgres 130 + COCOON_DATABASE_URL: postgres://cocoon:${POSTGRES_PASSWORD}@postgres:5432/cocoon?sslmode=disable 131 + depends_on: 132 + cocoon: 133 + condition: service_healthy 134 + entrypoint: ["/bin/sh", "/create-initial-invite.sh"] 135 + restart: "no" 136 + 137 + caddy: 138 + image: caddy:2-alpine 139 + container_name: cocoon-caddy 140 + ports: 141 + - "80:80" 142 + - "443:443" 143 + volumes: 144 + - ./Caddyfile.postgres:/etc/caddy/Caddyfile:ro 145 + - caddy_data:/data 146 + - caddy_config:/config 147 + restart: unless-stopped 148 + environment: 149 + COCOON_HOSTNAME: ${COCOON_HOSTNAME} 150 + CADDY_ACME_EMAIL: ${COCOON_CONTACT_EMAIL:-} 151 + 152 + volumes: 153 + postgres_data: 154 + driver: local 155 + caddy_data: 156 + driver: local 157 + caddy_config: 158 + driver: local
+7 -2
docker-compose.yaml
··· 49 50 # Server configuration 51 COCOON_ADDR: ":8080" 52 - COCOON_DB_NAME: /data/cocoon/cocoon.db 53 COCOON_BLOCKSTORE_VARIANT: ${COCOON_BLOCKSTORE_VARIANT:-sqlite} 54 55 # Optional: SMTP settings for email ··· 68 COCOON_S3_ENDPOINT: ${COCOON_S3_ENDPOINT:-} 69 COCOON_S3_ACCESS_KEY: ${COCOON_S3_ACCESS_KEY:-} 70 COCOON_S3_SECRET_KEY: ${COCOON_S3_SECRET_KEY:-} 71 72 # Optional: Fallback proxy 73 COCOON_FALLBACK_PROXY: ${COCOON_FALLBACK_PROXY:-} ··· 97 COCOON_CONTACT_EMAIL: ${COCOON_CONTACT_EMAIL} 98 COCOON_RELAYS: ${COCOON_RELAYS:-https://bsky.network} 99 COCOON_ADMIN_PASSWORD: ${COCOON_ADMIN_PASSWORD} 100 - COCOON_DB_NAME: /data/cocoon/cocoon.db 101 depends_on: 102 - init-keys 103 entrypoint: ["/bin/sh", "/create-initial-invite.sh"]
··· 49 50 # Server configuration 51 COCOON_ADDR: ":8080" 52 + COCOON_DB_TYPE: ${COCOON_DB_TYPE:-sqlite} 53 + COCOON_DB_NAME: ${COCOON_DB_NAME:-/data/cocoon/cocoon.db} 54 + COCOON_DATABASE_URL: ${COCOON_DATABASE_URL:-} 55 COCOON_BLOCKSTORE_VARIANT: ${COCOON_BLOCKSTORE_VARIANT:-sqlite} 56 57 # Optional: SMTP settings for email ··· 70 COCOON_S3_ENDPOINT: ${COCOON_S3_ENDPOINT:-} 71 COCOON_S3_ACCESS_KEY: ${COCOON_S3_ACCESS_KEY:-} 72 COCOON_S3_SECRET_KEY: ${COCOON_S3_SECRET_KEY:-} 73 + COCOON_S3_CDN_URL: ${COCOON_S3_CDN_URL:-} 74 75 # Optional: Fallback proxy 76 COCOON_FALLBACK_PROXY: ${COCOON_FALLBACK_PROXY:-} ··· 100 COCOON_CONTACT_EMAIL: ${COCOON_CONTACT_EMAIL} 101 COCOON_RELAYS: ${COCOON_RELAYS:-https://bsky.network} 102 COCOON_ADMIN_PASSWORD: ${COCOON_ADMIN_PASSWORD} 103 + COCOON_DB_TYPE: ${COCOON_DB_TYPE:-sqlite} 104 + COCOON_DB_NAME: ${COCOON_DB_NAME:-/data/cocoon/cocoon.db} 105 + COCOON_DATABASE_URL: ${COCOON_DATABASE_URL:-} 106 depends_on: 107 - init-keys 108 entrypoint: ["/bin/sh", "/create-initial-invite.sh"]
+19 -17
go.mod
··· 1 module github.com/haileyok/cocoon 2 3 - go 1.24.1 4 5 require ( 6 github.com/Azure/go-autorest/autorest/to v0.4.1 7 github.com/aws/aws-sdk-go v1.55.7 8 github.com/bluesky-social/indigo v0.0.0-20251009212240-20524de167fe 9 github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792 10 github.com/domodwyer/mailyak/v3 v3.6.2 11 github.com/go-pkgz/expirable-cache/v3 v3.0.0 12 github.com/go-playground/validator v9.31.0+incompatible 13 github.com/golang-jwt/jwt/v4 v4.5.2 14 - github.com/google/uuid v1.4.0 15 github.com/gorilla/sessions v1.4.0 16 github.com/gorilla/websocket v1.5.1 17 github.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b ··· 24 github.com/joho/godotenv v1.5.1 25 github.com/labstack/echo-contrib v0.17.4 26 github.com/labstack/echo/v4 v4.13.3 27 - github.com/lestrrat-go/jwx/v2 v2.0.12 28 github.com/multiformats/go-multihash v0.2.3 29 github.com/samber/slog-echo v1.16.1 30 github.com/urfave/cli/v2 v2.27.6 31 github.com/whyrusleeping/cbor-gen v0.2.1-0.20241030202151-b7a6831be65e 32 gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b 33 - golang.org/x/crypto v0.38.0 34 gorm.io/driver/sqlite v1.5.7 35 gorm.io/gorm v1.25.12 36 ) ··· 56 github.com/gorilla/securecookie v1.1.2 // indirect 57 github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed // indirect 58 github.com/hashicorp/go-cleanhttp v0.5.2 // indirect 59 - github.com/hashicorp/go-retryablehttp v0.7.5 // indirect 60 github.com/hashicorp/golang-lru v1.0.2 // indirect 61 github.com/ipfs/bbloom v0.0.4 // indirect 62 github.com/ipfs/go-blockservice v0.5.2 // indirect ··· 76 github.com/ipld/go-ipld-prime v0.21.0 // indirect 77 github.com/jackc/pgpassfile v1.0.0 // indirect 78 github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect 79 - github.com/jackc/pgx/v5 v5.5.0 // indirect 80 github.com/jackc/puddle/v2 v2.2.1 // indirect 81 github.com/jbenet/goprocess v0.1.4 // indirect 82 github.com/jinzhu/inflection v1.0.0 // indirect ··· 85 github.com/klauspost/cpuid/v2 v2.2.7 // indirect 86 github.com/labstack/gommon v0.4.2 // indirect 87 github.com/leodido/go-urn v1.4.0 // indirect 88 - github.com/lestrrat-go/blackmagic v1.0.1 // indirect 89 github.com/lestrrat-go/httpcc v1.0.1 // indirect 90 - github.com/lestrrat-go/httprc v1.0.4 // indirect 91 github.com/lestrrat-go/iter v1.0.2 // indirect 92 github.com/lestrrat-go/option v1.0.1 // indirect 93 github.com/mattn/go-colorable v0.1.14 // indirect ··· 102 github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 103 github.com/opentracing/opentracing-go v1.2.0 // indirect 104 github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f // indirect 105 - github.com/prometheus/client_golang v1.22.0 // indirect 106 github.com/prometheus/client_model v0.6.2 // indirect 107 - github.com/prometheus/common v0.63.0 // indirect 108 github.com/prometheus/procfs v0.16.1 // indirect 109 github.com/russross/blackfriday/v2 v2.1.0 // indirect 110 github.com/samber/lo v1.49.1 // indirect ··· 114 github.com/valyala/fasttemplate v1.2.2 // indirect 115 github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect 116 gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02 // indirect 117 - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 // indirect 118 go.opentelemetry.io/otel v1.29.0 // indirect 119 go.opentelemetry.io/otel/metric v1.29.0 // indirect 120 go.opentelemetry.io/otel/trace v1.29.0 // indirect 121 go.uber.org/atomic v1.11.0 // indirect 122 go.uber.org/multierr v1.11.0 // indirect 123 go.uber.org/zap v1.26.0 // indirect 124 - golang.org/x/net v0.40.0 // indirect 125 - golang.org/x/sync v0.14.0 // indirect 126 - golang.org/x/sys v0.33.0 // indirect 127 - golang.org/x/text v0.25.0 // indirect 128 golang.org/x/time v0.11.0 // indirect 129 golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect 130 - google.golang.org/protobuf v1.36.6 // indirect 131 gopkg.in/go-playground/assert.v1 v1.2.1 // indirect 132 gopkg.in/inf.v0 v0.9.1 // indirect 133 - gorm.io/driver/postgres v1.5.7 // indirect 134 lukechampine.com/blake3 v1.2.1 // indirect 135 )
··· 1 module github.com/haileyok/cocoon 2 3 + go 1.24.5 4 5 require ( 6 github.com/Azure/go-autorest/autorest/to v0.4.1 7 github.com/aws/aws-sdk-go v1.55.7 8 + github.com/bluesky-social/go-util v0.0.0-20251012040650-2ebbf57f5934 9 github.com/bluesky-social/indigo v0.0.0-20251009212240-20524de167fe 10 github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792 11 github.com/domodwyer/mailyak/v3 v3.6.2 12 github.com/go-pkgz/expirable-cache/v3 v3.0.0 13 github.com/go-playground/validator v9.31.0+incompatible 14 github.com/golang-jwt/jwt/v4 v4.5.2 15 + github.com/google/uuid v1.6.0 16 github.com/gorilla/sessions v1.4.0 17 github.com/gorilla/websocket v1.5.1 18 github.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b ··· 25 github.com/joho/godotenv v1.5.1 26 github.com/labstack/echo-contrib v0.17.4 27 github.com/labstack/echo/v4 v4.13.3 28 + github.com/lestrrat-go/jwx/v2 v2.0.21 29 github.com/multiformats/go-multihash v0.2.3 30 + github.com/prometheus/client_golang v1.23.2 31 github.com/samber/slog-echo v1.16.1 32 github.com/urfave/cli/v2 v2.27.6 33 github.com/whyrusleeping/cbor-gen v0.2.1-0.20241030202151-b7a6831be65e 34 gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b 35 + golang.org/x/crypto v0.41.0 36 + gorm.io/driver/postgres v1.5.7 37 gorm.io/driver/sqlite v1.5.7 38 gorm.io/gorm v1.25.12 39 ) ··· 59 github.com/gorilla/securecookie v1.1.2 // indirect 60 github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed // indirect 61 github.com/hashicorp/go-cleanhttp v0.5.2 // indirect 62 + github.com/hashicorp/go-retryablehttp v0.7.7 // indirect 63 github.com/hashicorp/golang-lru v1.0.2 // indirect 64 github.com/ipfs/bbloom v0.0.4 // indirect 65 github.com/ipfs/go-blockservice v0.5.2 // indirect ··· 79 github.com/ipld/go-ipld-prime v0.21.0 // indirect 80 github.com/jackc/pgpassfile v1.0.0 // indirect 81 github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect 82 + github.com/jackc/pgx/v5 v5.5.4 // indirect 83 github.com/jackc/puddle/v2 v2.2.1 // indirect 84 github.com/jbenet/goprocess v0.1.4 // indirect 85 github.com/jinzhu/inflection v1.0.0 // indirect ··· 88 github.com/klauspost/cpuid/v2 v2.2.7 // indirect 89 github.com/labstack/gommon v0.4.2 // indirect 90 github.com/leodido/go-urn v1.4.0 // indirect 91 + github.com/lestrrat-go/blackmagic v1.0.2 // indirect 92 github.com/lestrrat-go/httpcc v1.0.1 // indirect 93 + github.com/lestrrat-go/httprc v1.0.5 // indirect 94 github.com/lestrrat-go/iter v1.0.2 // indirect 95 github.com/lestrrat-go/option v1.0.1 // indirect 96 github.com/mattn/go-colorable v0.1.14 // indirect ··· 105 github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 106 github.com/opentracing/opentracing-go v1.2.0 // indirect 107 github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f // indirect 108 github.com/prometheus/client_model v0.6.2 // indirect 109 + github.com/prometheus/common v0.66.1 // indirect 110 github.com/prometheus/procfs v0.16.1 // indirect 111 github.com/russross/blackfriday/v2 v2.1.0 // indirect 112 github.com/samber/lo v1.49.1 // indirect ··· 116 github.com/valyala/fasttemplate v1.2.2 // indirect 117 github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect 118 gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02 // indirect 119 + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect 120 go.opentelemetry.io/otel v1.29.0 // indirect 121 go.opentelemetry.io/otel/metric v1.29.0 // indirect 122 go.opentelemetry.io/otel/trace v1.29.0 // indirect 123 go.uber.org/atomic v1.11.0 // indirect 124 go.uber.org/multierr v1.11.0 // indirect 125 go.uber.org/zap v1.26.0 // indirect 126 + go.yaml.in/yaml/v2 v2.4.2 // indirect 127 + golang.org/x/net v0.43.0 // indirect 128 + golang.org/x/sync v0.16.0 // indirect 129 + golang.org/x/sys v0.35.0 // indirect 130 + golang.org/x/text v0.28.0 // indirect 131 golang.org/x/time v0.11.0 // indirect 132 golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect 133 + google.golang.org/protobuf v1.36.9 // indirect 134 gopkg.in/go-playground/assert.v1 v1.2.1 // indirect 135 gopkg.in/inf.v0 v0.9.1 // indirect 136 lukechampine.com/blake3 v1.2.1 // indirect 137 )
+50 -74
go.sum
··· 16 github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 17 github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932 h1:mXoPYz/Ul5HYEDvkta6I8/rnYM5gSdSV2tJ6XbZuEtY= 18 github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932/go.mod h1:NOuUCSz6Q9T7+igc/hlvDOUdtWKryOrtFyIVABv/p7k= 19 github.com/bluesky-social/indigo v0.0.0-20251009212240-20524de167fe h1:VBhaqE5ewQgXbY5SfSWFZC/AwHFo7cHxZKFYi2ce9Yo= 20 github.com/bluesky-social/indigo v0.0.0-20251009212240-20524de167fe/go.mod h1:RuQVrCGm42QNsgumKaR6se+XkFKfCPNwdCiTvqKRUck= 21 github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY= ··· 34 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 35 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 36 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 37 - github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= 38 github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs= 39 github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= 40 github.com/domodwyer/mailyak/v3 v3.6.2 h1:x3tGMsyFhTCaxp6ycgR0FE/bu5QiNp+hetUuCOBXMn8= 41 github.com/domodwyer/mailyak/v3 v3.6.2/go.mod h1:lOm/u9CyCVWHeaAmHIdF4RiKVxKUT/H5XX10lIKAL6c= 42 github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= 43 github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= 44 github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= ··· 77 github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8= 78 github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo= 79 github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 80 - github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4= 81 - github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 82 github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= 83 github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= 84 github.com/gorilla/context v1.1.2 h1:WRkNAv2uoa03QNIc1A6u4O7DAGMUVoopZhkiXWA2V1o= ··· 95 github.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b/go.mod h1:VzxiSdG6j1pi7rwGm/xYI5RbtpBgM8sARDXlvEvxlu0= 96 github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= 97 github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= 98 - github.com/hashicorp/go-hclog v0.9.2 h1:CG6TE5H9/JXsFWJCfoIVpKFIkFe6ysEuHirp4DxCsHI= 99 - github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= 100 - github.com/hashicorp/go-retryablehttp v0.7.5 h1:bJj+Pj19UZMIweq/iie+1u5YCdGrnxCT9yvm0e+Nd5M= 101 - github.com/hashicorp/go-retryablehttp v0.7.5/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5wjtH1ewM9u8iYVjtX8= 102 github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c= 103 github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= 104 github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= ··· 172 github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= 173 github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= 174 github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= 175 - github.com/jackc/pgx/v5 v5.5.0 h1:NxstgwndsTRy7eq9/kqYc/BZh5w2hHJV86wjvO+1xPw= 176 - github.com/jackc/pgx/v5 v5.5.0/go.mod h1:Ig06C2Vu0t5qXC60W8sqIthScaEnFvojjj9dSljmHRA= 177 github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= 178 github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= 179 github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus= ··· 195 github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= 196 github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= 197 github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 198 github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= 199 github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= 200 github.com/koron/go-ssdp v0.0.3 h1:JivLMY45N76b4p/vsWGOKewBQu6uf39y8l+AQ7sDKx8= ··· 206 github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 207 github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 208 github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 209 github.com/labstack/echo-contrib v0.17.4 h1:g5mfsrJfJTKv+F5uNKCyrjLK7js+ZW6HTjg4FnDxxgk= 210 github.com/labstack/echo-contrib v0.17.4/go.mod h1:9O7ZPAHUeMGTOAfg80YqQduHzt0CzLak36PZRldYrZ0= 211 github.com/labstack/echo/v4 v4.13.3 h1:pwhpCPrTl5qry5HRdM5FwdXnhXSLSY+WE+YQSeCaafY= ··· 214 github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU= 215 github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= 216 github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= 217 - github.com/lestrrat-go/blackmagic v1.0.1 h1:lS5Zts+5HIC/8og6cGHb0uCcNCa3OUt1ygh3Qz2Fe80= 218 - github.com/lestrrat-go/blackmagic v1.0.1/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU= 219 github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE= 220 github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E= 221 - github.com/lestrrat-go/httprc v1.0.4 h1:bAZymwoZQb+Oq8MEbyipag7iSq6YIga8Wj6GOiJGdI8= 222 - github.com/lestrrat-go/httprc v1.0.4/go.mod h1:mwwz3JMTPBjHUkkDv/IGJ39aALInZLrhBp0X7KGUZlo= 223 github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI= 224 github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4= 225 - github.com/lestrrat-go/jwx/v2 v2.0.12 h1:3d589+5w/b9b7S3DneICPW16AqTyYXB7VRjgluSDWeA= 226 - github.com/lestrrat-go/jwx/v2 v2.0.12/go.mod h1:Mq4KN1mM7bp+5z/W5HS8aCNs5RKZ911G/0y2qUjAQuQ= 227 - github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= 228 github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU= 229 github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= 230 github.com/libp2p/go-buffer-pool v0.1.0 h1:oK4mSFcQz7cTQIfqbe4MIj9gLW+mnanjyFtc6cdF0Y8= ··· 289 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 290 github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f h1:VXTQfuJj9vKR4TCkEuWIckKvdHFeJH/huIFJ9/cXOB0= 291 github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f/go.mod h1:/zvteZs/GwLtCgZ4BL6CBsk9IKIlexP43ObX9AxTqTw= 292 - github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= 293 - github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= 294 github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= 295 github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= 296 - github.com/prometheus/common v0.63.0 h1:YR/EIY1o3mEFP/kZCD7iDMnLPlGyuU2Gb3HIcXnA98k= 297 - github.com/prometheus/common v0.63.0/go.mod h1:VVFF/fBIoToEnWRVkYoXEkq3R3paCoxG9PXP74SnV18= 298 github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= 299 github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= 300 github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= ··· 317 github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= 318 github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= 319 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 320 - github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 321 - github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 322 - github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 323 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 324 github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 325 github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 326 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 327 github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 328 - github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 329 - github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 330 - github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 331 - github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 332 github.com/urfave/cli v1.22.10/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= 333 github.com/urfave/cli/v2 v2.27.6 h1:VdRdS98FNhKZ8/Az8B7MTyGQmpIr36O1EHybx/LaZ4g= 334 github.com/urfave/cli/v2 v2.27.6/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ= ··· 349 github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 350 github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 351 github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 352 - github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 353 gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b h1:CzigHMRySiX3drau9C6Q5CAbNIApmLdat5jPMqChvDA= 354 gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b/go.mod h1:/y/V339mxv2sZmYYR64O07VuCpdNZqCTwO8ZcouTMI8= 355 gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02 h1:qwDnMxjkyLmAFgcfgTnfJrmYKWhHnci3GjDqcZp1M3Q= 356 gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02/go.mod h1:JTnUj0mpYiAsuZLmKjTx/ex3AtMowcCgnE7YNyCEP0I= 357 - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 h1:aFJWCqJMNjENlcleuuOkGAPH82y0yULBScfXcIEdS24= 358 - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1/go.mod h1:sEGXWArGqc3tVa+ekntsN65DmVbVeW+7lTKTjZF3/Fo= 359 go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw= 360 go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8= 361 go.opentelemetry.io/otel/metric v1.29.0 h1:vPf/HFWTNkPu1aYeIsc98l4ktOQaL6LeSoeV2g+8YLc= ··· 367 go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= 368 go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= 369 go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= 370 - go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk= 371 - go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo= 372 go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= 373 go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= 374 go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= ··· 378 go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI= 379 go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= 380 go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= 381 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 382 golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 383 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 384 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 385 - golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 386 - golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= 387 - golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= 388 - golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= 389 golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa h1:FRnLl4eNAQl8hwxVVC17teOw8kdjVDVAiFMtgUdTSRQ= 390 golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa/go.mod h1:zk2irFbV9DP96SEBUUAy67IdHUaZuSnrz1n472HUCLE= 391 golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= ··· 393 golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 394 golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 395 golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 396 - golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 397 - golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 398 - golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= 399 - golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 400 golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 401 golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 402 golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 403 golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 404 golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 405 - golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 406 golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= 407 - golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 408 - golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 409 - golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= 410 - golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= 411 - golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= 412 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 413 golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 414 golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 415 golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 416 - golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 417 - golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 418 - golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= 419 - golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= 420 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 421 golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 422 golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 423 golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 424 golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 425 golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 426 - golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 427 golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 428 - golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 429 - golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 430 - golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 431 golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 432 golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 433 - golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 434 - golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 435 - golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= 436 - golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 437 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 438 - golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 439 - golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 440 - golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= 441 - golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= 442 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 443 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 444 - golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 445 - golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 446 - golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 447 - golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= 448 - golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= 449 - golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= 450 golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= 451 golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= 452 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= ··· 459 golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 460 golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 461 golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= 462 - golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 463 - golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= 464 - golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= 465 - golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= 466 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 467 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 468 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 469 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 470 golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU= 471 golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= 472 - google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= 473 - google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= 474 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 475 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 476 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
··· 16 github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 17 github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932 h1:mXoPYz/Ul5HYEDvkta6I8/rnYM5gSdSV2tJ6XbZuEtY= 18 github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932/go.mod h1:NOuUCSz6Q9T7+igc/hlvDOUdtWKryOrtFyIVABv/p7k= 19 + github.com/bluesky-social/go-util v0.0.0-20251012040650-2ebbf57f5934 h1:btHMur2kTRgWEnCHn6LaI3BE9YRgsqTpwpJ1UdB7VEk= 20 + github.com/bluesky-social/go-util v0.0.0-20251012040650-2ebbf57f5934/go.mod h1:LWamyZfbQGW7PaVc5jumFfjgrshJ5mXgDUnR6fK7+BI= 21 github.com/bluesky-social/indigo v0.0.0-20251009212240-20524de167fe h1:VBhaqE5ewQgXbY5SfSWFZC/AwHFo7cHxZKFYi2ce9Yo= 22 github.com/bluesky-social/indigo v0.0.0-20251009212240-20524de167fe/go.mod h1:RuQVrCGm42QNsgumKaR6se+XkFKfCPNwdCiTvqKRUck= 23 github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY= ··· 36 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 37 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 38 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 39 github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs= 40 github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= 41 github.com/domodwyer/mailyak/v3 v3.6.2 h1:x3tGMsyFhTCaxp6ycgR0FE/bu5QiNp+hetUuCOBXMn8= 42 github.com/domodwyer/mailyak/v3 v3.6.2/go.mod h1:lOm/u9CyCVWHeaAmHIdF4RiKVxKUT/H5XX10lIKAL6c= 43 + github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= 44 + github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= 45 github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= 46 github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= 47 github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= ··· 80 github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8= 81 github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo= 82 github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 83 + github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 84 + github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 85 github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= 86 github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= 87 github.com/gorilla/context v1.1.2 h1:WRkNAv2uoa03QNIc1A6u4O7DAGMUVoopZhkiXWA2V1o= ··· 98 github.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b/go.mod h1:VzxiSdG6j1pi7rwGm/xYI5RbtpBgM8sARDXlvEvxlu0= 99 github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= 100 github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= 101 + github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= 102 + github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= 103 + github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU= 104 + github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk= 105 github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c= 106 github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= 107 github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= ··· 175 github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= 176 github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= 177 github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= 178 + github.com/jackc/pgx/v5 v5.5.4 h1:Xp2aQS8uXButQdnCMWNmvx6UysWQQC+u1EoizjguY+8= 179 + github.com/jackc/pgx/v5 v5.5.4/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A= 180 github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= 181 github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= 182 github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus= ··· 198 github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= 199 github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= 200 github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 201 + github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= 202 + github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= 203 github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= 204 github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= 205 github.com/koron/go-ssdp v0.0.3 h1:JivLMY45N76b4p/vsWGOKewBQu6uf39y8l+AQ7sDKx8= ··· 211 github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 212 github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 213 github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 214 + github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= 215 + github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= 216 github.com/labstack/echo-contrib v0.17.4 h1:g5mfsrJfJTKv+F5uNKCyrjLK7js+ZW6HTjg4FnDxxgk= 217 github.com/labstack/echo-contrib v0.17.4/go.mod h1:9O7ZPAHUeMGTOAfg80YqQduHzt0CzLak36PZRldYrZ0= 218 github.com/labstack/echo/v4 v4.13.3 h1:pwhpCPrTl5qry5HRdM5FwdXnhXSLSY+WE+YQSeCaafY= ··· 221 github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU= 222 github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= 223 github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= 224 + github.com/lestrrat-go/blackmagic v1.0.2 h1:Cg2gVSc9h7sz9NOByczrbUvLopQmXrfFx//N+AkAr5k= 225 + github.com/lestrrat-go/blackmagic v1.0.2/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU= 226 github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE= 227 github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E= 228 + github.com/lestrrat-go/httprc v1.0.5 h1:bsTfiH8xaKOJPrg1R+E3iE/AWZr/x0Phj9PBTG/OLUk= 229 + github.com/lestrrat-go/httprc v1.0.5/go.mod h1:mwwz3JMTPBjHUkkDv/IGJ39aALInZLrhBp0X7KGUZlo= 230 github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI= 231 github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4= 232 + github.com/lestrrat-go/jwx/v2 v2.0.21 h1:jAPKupy4uHgrHFEdjVjNkUgoBKtVDgrQPB/h55FHrR0= 233 + github.com/lestrrat-go/jwx/v2 v2.0.21/go.mod h1:09mLW8zto6bWL9GbwnqAli+ArLf+5M33QLQPDggkUWM= 234 github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU= 235 github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= 236 github.com/libp2p/go-buffer-pool v0.1.0 h1:oK4mSFcQz7cTQIfqbe4MIj9gLW+mnanjyFtc6cdF0Y8= ··· 295 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 296 github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f h1:VXTQfuJj9vKR4TCkEuWIckKvdHFeJH/huIFJ9/cXOB0= 297 github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f/go.mod h1:/zvteZs/GwLtCgZ4BL6CBsk9IKIlexP43ObX9AxTqTw= 298 + github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= 299 + github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= 300 github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= 301 github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= 302 + github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs= 303 + github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA= 304 github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= 305 github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= 306 github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= ··· 323 github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= 324 github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= 325 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 326 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 327 github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 328 github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 329 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 330 github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 331 + github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= 332 + github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= 333 github.com/urfave/cli v1.22.10/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= 334 github.com/urfave/cli/v2 v2.27.6 h1:VdRdS98FNhKZ8/Az8B7MTyGQmpIr36O1EHybx/LaZ4g= 335 github.com/urfave/cli/v2 v2.27.6/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ= ··· 350 github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 351 github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 352 github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 353 gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b h1:CzigHMRySiX3drau9C6Q5CAbNIApmLdat5jPMqChvDA= 354 gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b/go.mod h1:/y/V339mxv2sZmYYR64O07VuCpdNZqCTwO8ZcouTMI8= 355 gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02 h1:qwDnMxjkyLmAFgcfgTnfJrmYKWhHnci3GjDqcZp1M3Q= 356 gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02/go.mod h1:JTnUj0mpYiAsuZLmKjTx/ex3AtMowcCgnE7YNyCEP0I= 357 + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk= 358 + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw= 359 go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw= 360 go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8= 361 go.opentelemetry.io/otel/metric v1.29.0 h1:vPf/HFWTNkPu1aYeIsc98l4ktOQaL6LeSoeV2g+8YLc= ··· 367 go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= 368 go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= 369 go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= 370 + go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= 371 + go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= 372 go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= 373 go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= 374 go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= ··· 378 go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI= 379 go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= 380 go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= 381 + go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= 382 + go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= 383 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 384 golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 385 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 386 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 387 + golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= 388 + golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= 389 golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa h1:FRnLl4eNAQl8hwxVVC17teOw8kdjVDVAiFMtgUdTSRQ= 390 golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa/go.mod h1:zk2irFbV9DP96SEBUUAy67IdHUaZuSnrz1n472HUCLE= 391 golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= ··· 393 golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 394 golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 395 golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 396 + golang.org/x/mod v0.26.0 h1:EGMPT//Ezu+ylkCijjPc+f4Aih7sZvaAr+O3EHBxvZg= 397 + golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ= 398 golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 399 golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 400 golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 401 golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 402 golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 403 golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= 404 + golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= 405 + golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= 406 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 407 golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 408 golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 409 golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 410 + golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= 411 + golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= 412 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 413 golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 414 golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 415 golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 416 golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 417 golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 418 golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 419 golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 420 golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 421 + golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= 422 + golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 423 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 424 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 425 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 426 + golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= 427 + golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= 428 golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= 429 golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= 430 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= ··· 437 golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 438 golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 439 golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= 440 + golang.org/x/tools v0.35.0 h1:mBffYraMEf7aa0sB+NuKnuCy8qI/9Bughn8dC2Gu5r0= 441 + golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw= 442 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 443 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 444 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 445 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 446 golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU= 447 golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= 448 + google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw= 449 + google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= 450 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 451 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 452 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
+15 -14
internal/db/db.go
··· 1 package db 2 3 import ( 4 "sync" 5 6 "gorm.io/gorm" ··· 19 } 20 } 21 22 - func (db *DB) Create(value any, clauses []clause.Expression) *gorm.DB { 23 db.mu.Lock() 24 defer db.mu.Unlock() 25 - return db.cli.Clauses(clauses...).Create(value) 26 } 27 28 - func (db *DB) Save(value any, clauses []clause.Expression) *gorm.DB { 29 db.mu.Lock() 30 defer db.mu.Unlock() 31 - return db.cli.Clauses(clauses...).Save(value) 32 } 33 34 - func (db *DB) Exec(sql string, clauses []clause.Expression, values ...any) *gorm.DB { 35 db.mu.Lock() 36 defer db.mu.Unlock() 37 - return db.cli.Clauses(clauses...).Exec(sql, values...) 38 } 39 40 - func (db *DB) Raw(sql string, clauses []clause.Expression, values ...any) *gorm.DB { 41 - return db.cli.Clauses(clauses...).Raw(sql, values...) 42 } 43 44 func (db *DB) AutoMigrate(models ...any) error { 45 return db.cli.AutoMigrate(models...) 46 } 47 48 - func (db *DB) Delete(value any, clauses []clause.Expression) *gorm.DB { 49 db.mu.Lock() 50 defer db.mu.Unlock() 51 - return db.cli.Clauses(clauses...).Delete(value) 52 } 53 54 - func (db *DB) First(dest any, conds ...any) *gorm.DB { 55 - return db.cli.First(dest, conds...) 56 } 57 58 // TODO: this isn't actually good. we can commit even if the db is locked here. this is probably okay for the time being, but need to figure 59 // out a better solution. right now we only do this whenever we're importing a repo though so i'm mostly not worried, but it's still bad. 60 // e.g. when we do apply writes we should also be using a transcation but we don't right now 61 - func (db *DB) BeginDangerously() *gorm.DB { 62 - return db.cli.Begin() 63 } 64 65 func (db *DB) Lock() {
··· 1 package db 2 3 import ( 4 + "context" 5 "sync" 6 7 "gorm.io/gorm" ··· 20 } 21 } 22 23 + func (db *DB) Create(ctx context.Context, value any, clauses []clause.Expression) *gorm.DB { 24 db.mu.Lock() 25 defer db.mu.Unlock() 26 + return db.cli.WithContext(ctx).Clauses(clauses...).Create(value) 27 } 28 29 + func (db *DB) Save(ctx context.Context, value any, clauses []clause.Expression) *gorm.DB { 30 db.mu.Lock() 31 defer db.mu.Unlock() 32 + return db.cli.WithContext(ctx).Clauses(clauses...).Save(value) 33 } 34 35 + func (db *DB) Exec(ctx context.Context, sql string, clauses []clause.Expression, values ...any) *gorm.DB { 36 db.mu.Lock() 37 defer db.mu.Unlock() 38 + return db.cli.WithContext(ctx).Clauses(clauses...).Exec(sql, values...) 39 } 40 41 + func (db *DB) Raw(ctx context.Context, sql string, clauses []clause.Expression, values ...any) *gorm.DB { 42 + return db.cli.WithContext(ctx).Clauses(clauses...).Raw(sql, values...) 43 } 44 45 func (db *DB) AutoMigrate(models ...any) error { 46 return db.cli.AutoMigrate(models...) 47 } 48 49 + func (db *DB) Delete(ctx context.Context, value any, clauses []clause.Expression) *gorm.DB { 50 db.mu.Lock() 51 defer db.mu.Unlock() 52 + return db.cli.WithContext(ctx).Clauses(clauses...).Delete(value) 53 } 54 55 + func (db *DB) First(ctx context.Context, dest any, conds ...any) *gorm.DB { 56 + return db.cli.WithContext(ctx).First(dest, conds...) 57 } 58 59 // TODO: this isn't actually good. we can commit even if the db is locked here. this is probably okay for the time being, but need to figure 60 // out a better solution. right now we only do this whenever we're importing a repo though so i'm mostly not worried, but it's still bad. 61 // e.g. when we do apply writes we should also be using a transcation but we don't right now 62 + func (db *DB) BeginDangerously(ctx context.Context) *gorm.DB { 63 + return db.cli.WithContext(ctx).Begin() 64 } 65 66 func (db *DB) Lock() {
+30
metrics/metrics.go
···
··· 1 + package metrics 2 + 3 + import ( 4 + "github.com/prometheus/client_golang/prometheus" 5 + "github.com/prometheus/client_golang/prometheus/promauto" 6 + ) 7 + 8 + const ( 9 + NAMESPACE = "cocoon" 10 + ) 11 + 12 + var ( 13 + RelaysConnected = promauto.NewGaugeVec(prometheus.GaugeOpts{ 14 + Namespace: NAMESPACE, 15 + Name: "relays_connected", 16 + Help: "number of connected relays, by host", 17 + }, []string{"host"}) 18 + 19 + RelaySends = promauto.NewCounterVec(prometheus.CounterOpts{ 20 + Namespace: NAMESPACE, 21 + Name: "relay_sends", 22 + Help: "number of events sent to a relay, by host", 23 + }, []string{"host", "kind"}) 24 + 25 + RepoOperations = promauto.NewCounterVec(prometheus.CounterOpts{ 26 + Namespace: NAMESPACE, 27 + Name: "repo_operations", 28 + Help: "number of operations made against repos", 29 + }, []string{"kind"}) 30 + )
+19
models/models.go
··· 8 "github.com/bluesky-social/indigo/atproto/atcrypto" 9 ) 10 11 type Repo struct { 12 Did string `gorm:"primaryKey"` 13 CreatedAt time.Time ··· 21 PasswordResetCodeExpiresAt *time.Time 22 PlcOperationCode *string 23 PlcOperationCodeExpiresAt *time.Time 24 Password string 25 SigningKey []byte 26 Rev string 27 Root []byte 28 Preferences []byte 29 Deactivated bool 30 } 31 32 func (r *Repo) SignFor(ctx context.Context, did string, msg []byte) ([]byte, error) { ··· 117 Idx int `gorm:"primaryKey"` 118 Data []byte 119 }
··· 8 "github.com/bluesky-social/indigo/atproto/atcrypto" 9 ) 10 11 + type TwoFactorType string 12 + 13 + var ( 14 + TwoFactorTypeNone = TwoFactorType("none") 15 + TwoFactorTypeEmail = TwoFactorType("email") 16 + ) 17 + 18 type Repo struct { 19 Did string `gorm:"primaryKey"` 20 CreatedAt time.Time ··· 28 PasswordResetCodeExpiresAt *time.Time 29 PlcOperationCode *string 30 PlcOperationCodeExpiresAt *time.Time 31 + AccountDeleteCode *string 32 + AccountDeleteCodeExpiresAt *time.Time 33 Password string 34 SigningKey []byte 35 Rev string 36 Root []byte 37 Preferences []byte 38 Deactivated bool 39 + TwoFactorCode *string 40 + TwoFactorCodeExpiresAt *time.Time 41 + TwoFactorType TwoFactorType `gorm:"default:none"` 42 } 43 44 func (r *Repo) SignFor(ctx context.Context, did string, msg []byte) ([]byte, error) { ··· 129 Idx int `gorm:"primaryKey"` 130 Data []byte 131 } 132 + 133 + type ReservedKey struct { 134 + KeyDid string `gorm:"primaryKey"` 135 + Did *string `gorm:"index"` 136 + PrivateKey []byte 137 + CreatedAt time.Time `gorm:"index"` 138 + }
-1
oauth/client/manager.go
··· 72 } 73 74 jwks = k 75 - } else if metadata.JWKS != nil { 76 } else if metadata.JWKSURI != nil { 77 maybeJwks, err := cm.getClientJwks(ctx, clientId, *metadata.JWKSURI) 78 if err != nil {
··· 72 } 73 74 jwks = k 75 } else if metadata.JWKSURI != nil { 76 maybeJwks, err := cm.getClientJwks(ctx, clientId, *metadata.JWKSURI) 77 if err != nil {
+1 -1
oauth/dpop/jti_cache.go
··· 14 } 15 16 func newJTICache(size int) *jtiCache { 17 - cache := cache.NewCache[string, bool]().WithTTL(24 * time.Hour).WithLRU().WithTTL(constants.JTITtl) 18 return &jtiCache{ 19 cache: cache, 20 mu: sync.Mutex{},
··· 14 } 15 16 func newJTICache(size int) *jtiCache { 17 + cache := cache.NewCache[string, bool]().WithTTL(24 * time.Hour).WithLRU().WithTTL(constants.JTITtl).WithMaxKeys(size) 18 return &jtiCache{ 19 cache: cache, 20 mu: sync.Mutex{},
+4 -4
oauth/dpop/manager.go
··· 75 } 76 77 proof := extractProof(headers) 78 - 79 if proof == "" { 80 return nil, nil 81 } ··· 197 198 nonce, _ := claims["nonce"].(string) 199 if nonce == "" { 200 - // WARN: this _must_ be `use_dpop_nonce` for clients know they should make another request 201 return nil, ErrUseDpopNonce 202 } 203 204 if nonce != "" && !dm.nonce.Check(nonce) { 205 - // WARN: this _must_ be `use_dpop_nonce` so that clients will fetch a new nonce 206 return nil, ErrUseDpopNonce 207 } 208 ··· 237 } 238 239 func extractProof(headers http.Header) string { 240 - dpopHeaders := headers["Dpop"] 241 switch len(dpopHeaders) { 242 case 0: 243 return ""
··· 75 } 76 77 proof := extractProof(headers) 78 if proof == "" { 79 return nil, nil 80 } ··· 196 197 nonce, _ := claims["nonce"].(string) 198 if nonce == "" { 199 + // reference impl checks if self.nonce is not null before returning an error, but we always have a 200 + // nonce so we do not bother checking 201 return nil, ErrUseDpopNonce 202 } 203 204 if nonce != "" && !dm.nonce.Check(nonce) { 205 + // dpop nonce mismatch 206 return nil, ErrUseDpopNonce 207 } 208 ··· 237 } 238 239 func extractProof(headers http.Header) string { 240 + dpopHeaders := headers.Values("dpop") 241 switch len(dpopHeaders) { 242 case 0: 243 return ""
+3 -2
oauth/dpop/nonce.go
··· 102 } 103 104 func (n *Nonce) Check(nonce string) bool { 105 - n.mu.RLock() 106 - defer n.mu.RUnlock() 107 return nonce == n.prev || nonce == n.curr || nonce == n.next 108 }
··· 102 } 103 104 func (n *Nonce) Check(nonce string) bool { 105 + n.mu.Lock() 106 + defer n.mu.Unlock() 107 + n.rotate() 108 return nonce == n.prev || nonce == n.curr || nonce == n.next 109 }
+3 -3
oauth/provider/client_auth.go
··· 19 } 20 21 type AuthenticateClientRequestBase struct { 22 - ClientID string `form:"client_id" json:"client_id" validate:"required"` 23 - ClientAssertionType *string `form:"client_assertion_type" json:"client_assertion_type,omitempty"` 24 - ClientAssertion *string `form:"client_assertion" json:"client_assertion,omitempty"` 25 } 26 27 func (p *Provider) AuthenticateClient(ctx context.Context, req AuthenticateClientRequestBase, proof *dpop.Proof, opts *AuthenticateClientOptions) (*client.Client, *ClientAuth, error) {
··· 19 } 20 21 type AuthenticateClientRequestBase struct { 22 + ClientID string `form:"client_id" json:"client_id" query:"client_id" validate:"required"` 23 + ClientAssertionType *string `form:"client_assertion_type" json:"client_assertion_type,omitempty" query:"client_assertion_type"` 24 + ClientAssertion *string `form:"client_assertion" json:"client_assertion,omitempty" query:"client_assertion"` 25 } 26 27 func (p *Provider) AuthenticateClient(ctx context.Context, req AuthenticateClientRequestBase, proof *dpop.Proof, opts *AuthenticateClientOptions) (*client.Client, *ClientAuth, error) {
+9 -8
oauth/provider/models.go
··· 32 33 type ParRequest struct { 34 AuthenticateClientRequestBase 35 - ResponseType string `form:"response_type" json:"response_type" validate:"required"` 36 - CodeChallenge *string `form:"code_challenge" json:"code_challenge" validate:"required"` 37 - CodeChallengeMethod string `form:"code_challenge_method" json:"code_challenge_method" validate:"required"` 38 - State string `form:"state" json:"state" validate:"required"` 39 - RedirectURI string `form:"redirect_uri" json:"redirect_uri" validate:"required"` 40 - Scope string `form:"scope" json:"scope" validate:"required"` 41 - LoginHint *string `form:"login_hint" json:"login_hint,omitempty"` 42 - DpopJkt *string `form:"dpop_jkt" json:"dpop_jkt,omitempty"` 43 } 44 45 func (opr *ParRequest) Scan(value any) error {
··· 32 33 type ParRequest struct { 34 AuthenticateClientRequestBase 35 + ResponseType string `form:"response_type" json:"response_type" query:"response_type" validate:"required"` 36 + CodeChallenge *string `form:"code_challenge" json:"code_challenge" query:"code_challenge" validate:"required"` 37 + CodeChallengeMethod string `form:"code_challenge_method" json:"code_challenge_method" query:"code_challenge_method" validate:"required"` 38 + State string `form:"state" json:"state" query:"state" validate:"required"` 39 + RedirectURI string `form:"redirect_uri" json:"redirect_uri" query:"redirect_uri" validate:"required"` 40 + Scope string `form:"scope" json:"scope" query:"scope" validate:"required"` 41 + LoginHint *string `form:"login_hint" query:"login_hint" json:"login_hint,omitempty"` 42 + DpopJkt *string `form:"dpop_jkt" query:"dpop_jkt" json:"dpop_jkt,omitempty"` 43 + ResponseMode *string `form:"response_mode" json:"response_mode,omitempty" query:"response_mode"` 44 } 45 46 func (opr *ParRequest) Scan(value any) error {
+10 -8
server/common.go
··· 1 package server 2 3 import ( 4 "github.com/haileyok/cocoon/models" 5 ) 6 7 - func (s *Server) getActorByHandle(handle string) (*models.Actor, error) { 8 var actor models.Actor 9 - if err := s.db.First(&actor, models.Actor{Handle: handle}).Error; err != nil { 10 return nil, err 11 } 12 return &actor, nil 13 } 14 15 - func (s *Server) getRepoByEmail(email string) (*models.Repo, error) { 16 var repo models.Repo 17 - if err := s.db.First(&repo, models.Repo{Email: email}).Error; err != nil { 18 return nil, err 19 } 20 return &repo, nil 21 } 22 23 - func (s *Server) getRepoActorByEmail(email string) (*models.RepoActor, error) { 24 var repo models.RepoActor 25 - if err := s.db.Raw("SELECT r.*, a.* FROM repos r LEFT JOIN actors a ON r.did = a.did WHERE r.email= ?", nil, email).Scan(&repo).Error; err != nil { 26 return nil, err 27 } 28 return &repo, nil 29 } 30 31 - func (s *Server) getRepoActorByDid(did string) (*models.RepoActor, error) { 32 var repo models.RepoActor 33 - if err := s.db.Raw("SELECT r.*, a.* FROM repos r LEFT JOIN actors a ON r.did = a.did WHERE r.did = ?", nil, did).Scan(&repo).Error; err != nil { 34 return nil, err 35 } 36 return &repo, nil
··· 1 package server 2 3 import ( 4 + "context" 5 + 6 "github.com/haileyok/cocoon/models" 7 ) 8 9 + func (s *Server) getActorByHandle(ctx context.Context, handle string) (*models.Actor, error) { 10 var actor models.Actor 11 + if err := s.db.First(ctx, &actor, models.Actor{Handle: handle}).Error; err != nil { 12 return nil, err 13 } 14 return &actor, nil 15 } 16 17 + func (s *Server) getRepoByEmail(ctx context.Context, email string) (*models.Repo, error) { 18 var repo models.Repo 19 + if err := s.db.First(ctx, &repo, models.Repo{Email: email}).Error; err != nil { 20 return nil, err 21 } 22 return &repo, nil 23 } 24 25 + func (s *Server) getRepoActorByEmail(ctx context.Context, email string) (*models.RepoActor, error) { 26 var repo models.RepoActor 27 + if err := s.db.Raw(ctx, "SELECT r.*, a.* FROM repos r LEFT JOIN actors a ON r.did = a.did WHERE r.email= ?", nil, email).Scan(&repo).Error; err != nil { 28 return nil, err 29 } 30 return &repo, nil 31 } 32 33 + func (s *Server) getRepoActorByDid(ctx context.Context, did string) (*models.RepoActor, error) { 34 var repo models.RepoActor 35 + if err := s.db.Raw(ctx, "SELECT r.*, a.* FROM repos r LEFT JOIN actors a ON r.did = a.did WHERE r.did = ?", nil, did).Scan(&repo).Error; err != nil { 36 return nil, err 37 } 38 return &repo, nil
+4 -2
server/handle_account.go
··· 12 13 func (s *Server) handleAccount(e echo.Context) error { 14 ctx := e.Request().Context() 15 repo, sess, err := s.getSessionRepoOrErr(e) 16 if err != nil { 17 return e.Redirect(303, "/account/signin") ··· 20 oldestPossibleSession := time.Now().Add(constants.ConfidentialClientSessionLifetime) 21 22 var tokens []provider.OauthToken 23 - if err := s.db.Raw("SELECT * FROM oauth_tokens WHERE sub = ? AND created_at < ? ORDER BY created_at ASC", nil, repo.Repo.Did, oldestPossibleSession).Scan(&tokens).Error; err != nil { 24 - s.logger.Error("couldnt fetch oauth sessions for account", "did", repo.Repo.Did, "error", err) 25 sess.AddFlash("Unable to fetch sessions. See server logs for more details.", "error") 26 sess.Save(e.Request(), e.Response()) 27 return e.Render(200, "account.html", map[string]any{
··· 12 13 func (s *Server) handleAccount(e echo.Context) error { 14 ctx := e.Request().Context() 15 + logger := s.logger.With("name", "handleAuth") 16 + 17 repo, sess, err := s.getSessionRepoOrErr(e) 18 if err != nil { 19 return e.Redirect(303, "/account/signin") ··· 22 oldestPossibleSession := time.Now().Add(constants.ConfidentialClientSessionLifetime) 23 24 var tokens []provider.OauthToken 25 + 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 { 26 + logger.Error("couldnt fetch oauth sessions for account", "did", repo.Repo.Did, "error", err) 27 sess.AddFlash("Unable to fetch sessions. See server logs for more details.", "error") 28 sess.Save(e.Request(), e.Response()) 29 return e.Render(200, "account.html", map[string]any{
+8 -5
server/handle_account_revoke.go
··· 5 "github.com/labstack/echo/v4" 6 ) 7 8 - type AccountRevokeRequest struct { 9 Token string `form:"token"` 10 } 11 12 func (s *Server) handleAccountRevoke(e echo.Context) error { 13 - var req AccountRevokeRequest 14 if err := e.Bind(&req); err != nil { 15 - s.logger.Error("could not bind account revoke request", "error", err) 16 return helpers.ServerError(e, nil) 17 } 18 ··· 21 return e.Redirect(303, "/account/signin") 22 } 23 24 - if err := s.db.Exec("DELETE FROM oauth_tokens WHERE sub = ? AND token = ?", nil, repo.Repo.Did, req.Token).Error; err != nil { 25 - s.logger.Error("couldnt delete oauth session for account", "did", repo.Repo.Did, "token", req.Token, "error", err) 26 sess.AddFlash("Unable to revoke session. See server logs for more details.", "error") 27 sess.Save(e.Request(), e.Response()) 28 return e.Redirect(303, "/account")
··· 5 "github.com/labstack/echo/v4" 6 ) 7 8 + type AccountRevokeInput struct { 9 Token string `form:"token"` 10 } 11 12 func (s *Server) handleAccountRevoke(e echo.Context) error { 13 + ctx := e.Request().Context() 14 + logger := s.logger.With("name", "handleAcocuntRevoke") 15 + 16 + var req AccountRevokeInput 17 if err := e.Bind(&req); err != nil { 18 + logger.Error("could not bind account revoke request", "error", err) 19 return helpers.ServerError(e, nil) 20 } 21 ··· 24 return e.Redirect(303, "/account/signin") 25 } 26 27 + if err := s.db.Exec(ctx, "DELETE FROM oauth_tokens WHERE sub = ? AND token = ?", nil, repo.Repo.Did, req.Token).Error; err != nil { 28 + logger.Error("couldnt delete oauth session for account", "did", repo.Repo.Did, "token", req.Token, "error", err) 29 sess.AddFlash("Unable to revoke session. See server logs for more details.", "error") 30 sess.Save(e.Request(), e.Response()) 31 return e.Redirect(303, "/account")
+68 -16
server/handle_account_signin.go
··· 2 3 import ( 4 "errors" 5 "strings" 6 7 "github.com/bluesky-social/indigo/atproto/syntax" 8 "github.com/gorilla/sessions" ··· 14 "gorm.io/gorm" 15 ) 16 17 - type OauthSigninRequest struct { 18 - Username string `form:"username"` 19 - Password string `form:"password"` 20 - QueryParams string `form:"query_params"` 21 } 22 23 func (s *Server) getSessionRepoOrErr(e echo.Context) (*models.RepoActor, *sessions.Session, error) { 24 sess, err := session.Get("session", e) 25 if err != nil { 26 return nil, nil, err ··· 31 return nil, sess, errors.New("did was not set in session") 32 } 33 34 - repo, err := s.getRepoActorByDid(did) 35 if err != nil { 36 return nil, sess, err 37 } ··· 42 func getFlashesFromSession(e echo.Context, sess *sessions.Session) map[string]any { 43 defer sess.Save(e.Request(), e.Response()) 44 return map[string]any{ 45 - "errors": sess.Flashes("error"), 46 - "successes": sess.Flashes("success"), 47 } 48 } 49 ··· 60 } 61 62 func (s *Server) handleAccountSigninPost(e echo.Context) error { 63 - var req OauthSigninRequest 64 if err := e.Bind(&req); err != nil { 65 - s.logger.Error("error binding sign in req", "error", err) 66 return helpers.ServerError(e, nil) 67 } 68 ··· 76 idtype = "handle" 77 } else { 78 idtype = "email" 79 } 80 81 // TODO: we should make this a helper since we do it for the base create_session as well ··· 83 var err error 84 switch idtype { 85 case "did": 86 - err = s.db.Raw("SELECT r.*, a.* FROM repos r LEFT JOIN actors a ON r.did = a.did WHERE r.did = ?", nil, req.Username).Scan(&repo).Error 87 case "handle": 88 - err = s.db.Raw("SELECT r.*, a.* FROM actors a LEFT JOIN repos r ON a.did = r.did WHERE a.handle = ?", nil, req.Username).Scan(&repo).Error 89 case "email": 90 - err = s.db.Raw("SELECT r.*, a.* FROM repos r LEFT JOIN actors a ON r.did = a.did WHERE r.email = ?", nil, req.Username).Scan(&repo).Error 91 } 92 if err != nil { 93 if err == gorm.ErrRecordNotFound { ··· 96 sess.AddFlash("Something went wrong!", "error") 97 } 98 sess.Save(e.Request(), e.Response()) 99 - return e.Redirect(303, "/account/signin") 100 } 101 102 if err := bcrypt.CompareHashAndPassword([]byte(repo.Password), []byte(req.Password)); err != nil { ··· 106 sess.AddFlash("Something went wrong!", "error") 107 } 108 sess.Save(e.Request(), e.Response()) 109 - return e.Redirect(303, "/account/signin") 110 } 111 112 sess.Options = &sessions.Options{ ··· 122 return err 123 } 124 125 - if req.QueryParams != "" { 126 - return e.Redirect(303, "/oauth/authorize?"+req.QueryParams) 127 } else { 128 return e.Redirect(303, "/account") 129 }
··· 2 3 import ( 4 "errors" 5 + "fmt" 6 "strings" 7 + "time" 8 9 "github.com/bluesky-social/indigo/atproto/syntax" 10 "github.com/gorilla/sessions" ··· 16 "gorm.io/gorm" 17 ) 18 19 + type OauthSigninInput struct { 20 + Username string `form:"username"` 21 + Password string `form:"password"` 22 + AuthFactorToken string `form:"token"` 23 + QueryParams string `form:"query_params"` 24 } 25 26 func (s *Server) getSessionRepoOrErr(e echo.Context) (*models.RepoActor, *sessions.Session, error) { 27 + ctx := e.Request().Context() 28 + 29 sess, err := session.Get("session", e) 30 if err != nil { 31 return nil, nil, err ··· 36 return nil, sess, errors.New("did was not set in session") 37 } 38 39 + repo, err := s.getRepoActorByDid(ctx, did) 40 if err != nil { 41 return nil, sess, err 42 } ··· 47 func getFlashesFromSession(e echo.Context, sess *sessions.Session) map[string]any { 48 defer sess.Save(e.Request(), e.Response()) 49 return map[string]any{ 50 + "errors": sess.Flashes("error"), 51 + "successes": sess.Flashes("success"), 52 + "tokenrequired": sess.Flashes("tokenrequired"), 53 } 54 } 55 ··· 66 } 67 68 func (s *Server) handleAccountSigninPost(e echo.Context) error { 69 + ctx := e.Request().Context() 70 + logger := s.logger.With("name", "handleAccountSigninPost") 71 + 72 + var req OauthSigninInput 73 if err := e.Bind(&req); err != nil { 74 + logger.Error("error binding sign in req", "error", err) 75 return helpers.ServerError(e, nil) 76 } 77 ··· 85 idtype = "handle" 86 } else { 87 idtype = "email" 88 + } 89 + 90 + queryParams := "" 91 + if req.QueryParams != "" { 92 + queryParams = fmt.Sprintf("?%s", req.QueryParams) 93 } 94 95 // TODO: we should make this a helper since we do it for the base create_session as well ··· 97 var err error 98 switch idtype { 99 case "did": 100 + 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 101 case "handle": 102 + 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 103 case "email": 104 + 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 105 } 106 if err != nil { 107 if err == gorm.ErrRecordNotFound { ··· 110 sess.AddFlash("Something went wrong!", "error") 111 } 112 sess.Save(e.Request(), e.Response()) 113 + return e.Redirect(303, "/account/signin"+queryParams) 114 } 115 116 if err := bcrypt.CompareHashAndPassword([]byte(repo.Password), []byte(req.Password)); err != nil { ··· 120 sess.AddFlash("Something went wrong!", "error") 121 } 122 sess.Save(e.Request(), e.Response()) 123 + return e.Redirect(303, "/account/signin"+queryParams) 124 + } 125 + 126 + // if repo requires 2FA token and one hasn't been provided, return error prompting for one 127 + if repo.TwoFactorType != models.TwoFactorTypeNone && req.AuthFactorToken == "" { 128 + err = s.createAndSendTwoFactorCode(ctx, repo) 129 + if err != nil { 130 + sess.AddFlash("Something went wrong!", "error") 131 + sess.Save(e.Request(), e.Response()) 132 + return e.Redirect(303, "/account/signin"+queryParams) 133 + } 134 + 135 + sess.AddFlash("requires 2FA token", "tokenrequired") 136 + sess.Save(e.Request(), e.Response()) 137 + return e.Redirect(303, "/account/signin"+queryParams) 138 + } 139 + 140 + // if 2FAis required, now check that the one provided is valid 141 + if repo.TwoFactorType != models.TwoFactorTypeNone { 142 + if repo.TwoFactorCode == nil || repo.TwoFactorCodeExpiresAt == nil { 143 + err = s.createAndSendTwoFactorCode(ctx, repo) 144 + if err != nil { 145 + sess.AddFlash("Something went wrong!", "error") 146 + sess.Save(e.Request(), e.Response()) 147 + return e.Redirect(303, "/account/signin"+queryParams) 148 + } 149 + 150 + sess.AddFlash("requires 2FA token", "tokenrequired") 151 + sess.Save(e.Request(), e.Response()) 152 + return e.Redirect(303, "/account/signin"+queryParams) 153 + } 154 + 155 + if *repo.TwoFactorCode != req.AuthFactorToken { 156 + return helpers.InvalidTokenError(e) 157 + } 158 + 159 + if time.Now().UTC().After(*repo.TwoFactorCodeExpiresAt) { 160 + return helpers.ExpiredTokenError(e) 161 + } 162 } 163 164 sess.Options = &sessions.Options{ ··· 174 return err 175 } 176 177 + if queryParams != "" { 178 + return e.Redirect(303, "/oauth/authorize"+queryParams) 179 } else { 180 return e.Redirect(303, "/account") 181 }
+3 -1
server/handle_actor_put_preferences.go
··· 10 // This is kinda lame. Not great to implement app.bsky in the pds, but alas 11 12 func (s *Server) handleActorPutPreferences(e echo.Context) error { 13 repo := e.Get("repo").(*models.RepoActor) 14 15 var prefs map[string]any ··· 22 return err 23 } 24 25 - if err := s.db.Exec("UPDATE repos SET preferences = ? WHERE did = ?", nil, b, repo.Repo.Did).Error; err != nil { 26 return err 27 } 28
··· 10 // This is kinda lame. Not great to implement app.bsky in the pds, but alas 11 12 func (s *Server) handleActorPutPreferences(e echo.Context) error { 13 + ctx := e.Request().Context() 14 + 15 repo := e.Get("repo").(*models.RepoActor) 16 17 var prefs map[string]any ··· 24 return err 25 } 26 27 + if err := s.db.Exec(ctx, "UPDATE repos SET preferences = ? WHERE did = ?", nil, b, repo.Repo.Did).Error; err != nil { 28 return err 29 } 30
+6 -3
server/handle_identity_request_plc_operation.go
··· 10 ) 11 12 func (s *Server) handleIdentityRequestPlcOperationSignature(e echo.Context) error { 13 urepo := e.Get("repo").(*models.RepoActor) 14 15 code := fmt.Sprintf("%s-%s", helpers.RandomVarchar(5), helpers.RandomVarchar(5)) 16 eat := time.Now().Add(10 * time.Minute).UTC() 17 18 - if err := s.db.Exec("UPDATE repos SET plc_operation_code = ?, plc_operation_code_expires_at = ? WHERE did = ?", nil, code, eat, urepo.Repo.Did).Error; err != nil { 19 - s.logger.Error("error updating user", "error", err) 20 return helpers.ServerError(e, nil) 21 } 22 23 if err := s.sendPlcTokenReset(urepo.Email, urepo.Handle, code); err != nil { 24 - s.logger.Error("error sending mail", "error", err) 25 return helpers.ServerError(e, nil) 26 } 27
··· 10 ) 11 12 func (s *Server) handleIdentityRequestPlcOperationSignature(e echo.Context) error { 13 + ctx := e.Request().Context() 14 + logger := s.logger.With("name", "handleIdentityRequestPlcOperationSignature") 15 + 16 urepo := e.Get("repo").(*models.RepoActor) 17 18 code := fmt.Sprintf("%s-%s", helpers.RandomVarchar(5), helpers.RandomVarchar(5)) 19 eat := time.Now().Add(10 * time.Minute).UTC() 20 21 + if err := s.db.Exec(ctx, "UPDATE repos SET plc_operation_code = ?, plc_operation_code_expires_at = ? WHERE did = ?", nil, code, eat, urepo.Repo.Did).Error; err != nil { 22 + logger.Error("error updating user", "error", err) 23 return helpers.ServerError(e, nil) 24 } 25 26 if err := s.sendPlcTokenReset(urepo.Email, urepo.Handle, code); err != nil { 27 + logger.Error("error sending mail", "error", err) 28 return helpers.ServerError(e, nil) 29 } 30
+8 -6
server/handle_identity_sign_plc_operation.go
··· 27 } 28 29 func (s *Server) handleSignPlcOperation(e echo.Context) error { 30 repo := e.Get("repo").(*models.RepoActor) 31 32 var req ComAtprotoSignPlcOperationRequest 33 if err := e.Bind(&req); err != nil { 34 - s.logger.Error("error binding", "error", err) 35 return helpers.ServerError(e, nil) 36 } 37 ··· 54 ctx := context.WithValue(e.Request().Context(), "skip-cache", true) 55 log, err := identity.FetchDidAuditLog(ctx, nil, repo.Repo.Did) 56 if err != nil { 57 - s.logger.Error("error fetching doc", "error", err) 58 return helpers.ServerError(e, nil) 59 } 60 ··· 83 84 k, err := atcrypto.ParsePrivateBytesK256(repo.SigningKey) 85 if err != nil { 86 - s.logger.Error("error parsing signing key", "error", err) 87 return helpers.ServerError(e, nil) 88 } 89 90 if err := s.plcClient.SignOp(k, &op); err != nil { 91 - s.logger.Error("error signing plc operation", "error", err) 92 return helpers.ServerError(e, nil) 93 } 94 95 - if err := s.db.Exec("UPDATE repos SET plc_operation_code = NULL, plc_operation_code_expires_at = NULL WHERE did = ?", nil, repo.Repo.Did).Error; err != nil { 96 - s.logger.Error("error updating repo", "error", err) 97 return helpers.ServerError(e, nil) 98 } 99
··· 27 } 28 29 func (s *Server) handleSignPlcOperation(e echo.Context) error { 30 + logger := s.logger.With("name", "handleSignPlcOperation") 31 + 32 repo := e.Get("repo").(*models.RepoActor) 33 34 var req ComAtprotoSignPlcOperationRequest 35 if err := e.Bind(&req); err != nil { 36 + logger.Error("error binding", "error", err) 37 return helpers.ServerError(e, nil) 38 } 39 ··· 56 ctx := context.WithValue(e.Request().Context(), "skip-cache", true) 57 log, err := identity.FetchDidAuditLog(ctx, nil, repo.Repo.Did) 58 if err != nil { 59 + logger.Error("error fetching doc", "error", err) 60 return helpers.ServerError(e, nil) 61 } 62 ··· 85 86 k, err := atcrypto.ParsePrivateBytesK256(repo.SigningKey) 87 if err != nil { 88 + logger.Error("error parsing signing key", "error", err) 89 return helpers.ServerError(e, nil) 90 } 91 92 if err := s.plcClient.SignOp(k, &op); err != nil { 93 + logger.Error("error signing plc operation", "error", err) 94 return helpers.ServerError(e, nil) 95 } 96 97 + if err := s.db.Exec(ctx, "UPDATE repos SET plc_operation_code = NULL, plc_operation_code_expires_at = NULL WHERE did = ?", nil, repo.Repo.Did).Error; err != nil { 98 + logger.Error("error updating repo", "error", err) 99 return helpers.ServerError(e, nil) 100 } 101
+6 -4
server/handle_identity_submit_plc_operation.go
··· 21 } 22 23 func (s *Server) handleSubmitPlcOperation(e echo.Context) error { 24 repo := e.Get("repo").(*models.RepoActor) 25 26 var req ComAtprotoSubmitPlcOperationRequest 27 if err := e.Bind(&req); err != nil { 28 - s.logger.Error("error binding", "error", err) 29 return helpers.ServerError(e, nil) 30 } 31 ··· 40 41 k, err := atcrypto.ParsePrivateBytesK256(repo.SigningKey) 42 if err != nil { 43 - s.logger.Error("error parsing key", "error", err) 44 return helpers.ServerError(e, nil) 45 } 46 required, err := s.plcClient.CreateDidCredentials(k, "", repo.Actor.Handle) 47 if err != nil { 48 - s.logger.Error("error crating did credentials", "error", err) 49 return helpers.ServerError(e, nil) 50 } 51 ··· 72 } 73 74 if err := s.passport.BustDoc(context.TODO(), repo.Repo.Did); err != nil { 75 - s.logger.Warn("error busting did doc", "error", err) 76 } 77 78 s.evtman.AddEvent(context.TODO(), &events.XRPCStreamEvent{
··· 21 } 22 23 func (s *Server) handleSubmitPlcOperation(e echo.Context) error { 24 + logger := s.logger.With("name", "handleIdentitySubmitPlcOperation") 25 + 26 repo := e.Get("repo").(*models.RepoActor) 27 28 var req ComAtprotoSubmitPlcOperationRequest 29 if err := e.Bind(&req); err != nil { 30 + logger.Error("error binding", "error", err) 31 return helpers.ServerError(e, nil) 32 } 33 ··· 42 43 k, err := atcrypto.ParsePrivateBytesK256(repo.SigningKey) 44 if err != nil { 45 + logger.Error("error parsing key", "error", err) 46 return helpers.ServerError(e, nil) 47 } 48 required, err := s.plcClient.CreateDidCredentials(k, "", repo.Actor.Handle) 49 if err != nil { 50 + logger.Error("error crating did credentials", "error", err) 51 return helpers.ServerError(e, nil) 52 } 53 ··· 74 } 75 76 if err := s.passport.BustDoc(context.TODO(), repo.Repo.Did); err != nil { 77 + logger.Warn("error busting did doc", "error", err) 78 } 79 80 s.evtman.AddEvent(context.TODO(), &events.XRPCStreamEvent{
+8 -6
server/handle_identity_update_handle.go
··· 22 } 23 24 func (s *Server) handleIdentityUpdateHandle(e echo.Context) error { 25 repo := e.Get("repo").(*models.RepoActor) 26 27 var req ComAtprotoIdentityUpdateHandleRequest 28 if err := e.Bind(&req); err != nil { 29 - s.logger.Error("error binding", "error", err) 30 return helpers.ServerError(e, nil) 31 } 32 ··· 41 if strings.HasPrefix(repo.Repo.Did, "did:plc:") { 42 log, err := identity.FetchDidAuditLog(ctx, nil, repo.Repo.Did) 43 if err != nil { 44 - s.logger.Error("error fetching doc", "error", err) 45 return helpers.ServerError(e, nil) 46 } 47 ··· 68 69 k, err := atcrypto.ParsePrivateBytesK256(repo.SigningKey) 70 if err != nil { 71 - s.logger.Error("error parsing signing key", "error", err) 72 return helpers.ServerError(e, nil) 73 } 74 ··· 82 } 83 84 if err := s.passport.BustDoc(context.TODO(), repo.Repo.Did); err != nil { 85 - s.logger.Warn("error busting did doc", "error", err) 86 } 87 88 s.evtman.AddEvent(context.TODO(), &events.XRPCStreamEvent{ ··· 94 }, 95 }) 96 97 - if err := s.db.Exec("UPDATE actors SET handle = ? WHERE did = ?", nil, req.Handle, repo.Repo.Did).Error; err != nil { 98 - s.logger.Error("error updating handle in db", "error", err) 99 return helpers.ServerError(e, nil) 100 } 101
··· 22 } 23 24 func (s *Server) handleIdentityUpdateHandle(e echo.Context) error { 25 + logger := s.logger.With("name", "handleIdentityUpdateHandle") 26 + 27 repo := e.Get("repo").(*models.RepoActor) 28 29 var req ComAtprotoIdentityUpdateHandleRequest 30 if err := e.Bind(&req); err != nil { 31 + logger.Error("error binding", "error", err) 32 return helpers.ServerError(e, nil) 33 } 34 ··· 43 if strings.HasPrefix(repo.Repo.Did, "did:plc:") { 44 log, err := identity.FetchDidAuditLog(ctx, nil, repo.Repo.Did) 45 if err != nil { 46 + logger.Error("error fetching doc", "error", err) 47 return helpers.ServerError(e, nil) 48 } 49 ··· 70 71 k, err := atcrypto.ParsePrivateBytesK256(repo.SigningKey) 72 if err != nil { 73 + logger.Error("error parsing signing key", "error", err) 74 return helpers.ServerError(e, nil) 75 } 76 ··· 84 } 85 86 if err := s.passport.BustDoc(context.TODO(), repo.Repo.Did); err != nil { 87 + logger.Warn("error busting did doc", "error", err) 88 } 89 90 s.evtman.AddEvent(context.TODO(), &events.XRPCStreamEvent{ ··· 96 }, 97 }) 98 99 + if err := s.db.Exec(ctx, "UPDATE actors SET handle = ? WHERE did = ?", nil, req.Handle, repo.Repo.Did).Error; err != nil { 100 + logger.Error("error updating handle in db", "error", err) 101 return helpers.ServerError(e, nil) 102 } 103
+14 -11
server/handle_import_repo.go
··· 18 ) 19 20 func (s *Server) handleRepoImportRepo(e echo.Context) error { 21 urepo := e.Get("repo").(*models.RepoActor) 22 23 b, err := io.ReadAll(e.Request().Body) 24 if err != nil { 25 - s.logger.Error("could not read bytes in import request", "error", err) 26 return helpers.ServerError(e, nil) 27 } 28 ··· 30 31 cs, err := car.NewCarReader(bytes.NewReader(b)) 32 if err != nil { 33 - s.logger.Error("could not read car in import request", "error", err) 34 return helpers.ServerError(e, nil) 35 } 36 37 orderedBlocks := []blocks.Block{} 38 currBlock, err := cs.Next() 39 if err != nil { 40 - s.logger.Error("could not get first block from car", "error", err) 41 return helpers.ServerError(e, nil) 42 } 43 currBlockCt := 1 44 45 for currBlock != nil { 46 - s.logger.Info("someone is importing their repo", "block", currBlockCt) 47 orderedBlocks = append(orderedBlocks, currBlock) 48 next, _ := cs.Next() 49 currBlock = next ··· 53 slices.Reverse(orderedBlocks) 54 55 if err := bs.PutMany(context.TODO(), orderedBlocks); err != nil { 56 - s.logger.Error("could not insert blocks", "error", err) 57 return helpers.ServerError(e, nil) 58 } 59 60 r, err := repo.OpenRepo(context.TODO(), bs, cs.Header.Roots[0]) 61 if err != nil { 62 - s.logger.Error("could not open repo", "error", err) 63 return helpers.ServerError(e, nil) 64 } 65 66 - tx := s.db.BeginDangerously() 67 68 clock := syntax.NewTIDClock(0) 69 ··· 74 cidStr := cid.String() 75 b, err := bs.Get(context.TODO(), cid) 76 if err != nil { 77 - s.logger.Error("record bytes don't exist in blockstore", "error", err) 78 return helpers.ServerError(e, nil) 79 } 80 ··· 94 return nil 95 }); err != nil { 96 tx.Rollback() 97 - s.logger.Error("record bytes don't exist in blockstore", "error", err) 98 return helpers.ServerError(e, nil) 99 } 100 ··· 102 103 root, rev, err := r.Commit(context.TODO(), urepo.SignFor) 104 if err != nil { 105 - s.logger.Error("error committing", "error", err) 106 return helpers.ServerError(e, nil) 107 } 108 109 if err := s.UpdateRepo(context.TODO(), urepo.Repo.Did, root, rev); err != nil { 110 - s.logger.Error("error updating repo after commit", "error", err) 111 return helpers.ServerError(e, nil) 112 } 113
··· 18 ) 19 20 func (s *Server) handleRepoImportRepo(e echo.Context) error { 21 + ctx := e.Request().Context() 22 + logger := s.logger.With("name", "handleImportRepo") 23 + 24 urepo := e.Get("repo").(*models.RepoActor) 25 26 b, err := io.ReadAll(e.Request().Body) 27 if err != nil { 28 + logger.Error("could not read bytes in import request", "error", err) 29 return helpers.ServerError(e, nil) 30 } 31 ··· 33 34 cs, err := car.NewCarReader(bytes.NewReader(b)) 35 if err != nil { 36 + logger.Error("could not read car in import request", "error", err) 37 return helpers.ServerError(e, nil) 38 } 39 40 orderedBlocks := []blocks.Block{} 41 currBlock, err := cs.Next() 42 if err != nil { 43 + logger.Error("could not get first block from car", "error", err) 44 return helpers.ServerError(e, nil) 45 } 46 currBlockCt := 1 47 48 for currBlock != nil { 49 + logger.Info("someone is importing their repo", "block", currBlockCt) 50 orderedBlocks = append(orderedBlocks, currBlock) 51 next, _ := cs.Next() 52 currBlock = next ··· 56 slices.Reverse(orderedBlocks) 57 58 if err := bs.PutMany(context.TODO(), orderedBlocks); err != nil { 59 + logger.Error("could not insert blocks", "error", err) 60 return helpers.ServerError(e, nil) 61 } 62 63 r, err := repo.OpenRepo(context.TODO(), bs, cs.Header.Roots[0]) 64 if err != nil { 65 + logger.Error("could not open repo", "error", err) 66 return helpers.ServerError(e, nil) 67 } 68 69 + tx := s.db.BeginDangerously(ctx) 70 71 clock := syntax.NewTIDClock(0) 72 ··· 77 cidStr := cid.String() 78 b, err := bs.Get(context.TODO(), cid) 79 if err != nil { 80 + logger.Error("record bytes don't exist in blockstore", "error", err) 81 return helpers.ServerError(e, nil) 82 } 83 ··· 97 return nil 98 }); err != nil { 99 tx.Rollback() 100 + logger.Error("record bytes don't exist in blockstore", "error", err) 101 return helpers.ServerError(e, nil) 102 } 103 ··· 105 106 root, rev, err := r.Commit(context.TODO(), urepo.SignFor) 107 if err != nil { 108 + logger.Error("error committing", "error", err) 109 return helpers.ServerError(e, nil) 110 } 111 112 if err := s.UpdateRepo(context.TODO(), urepo.Repo.Did, root, rev); err != nil { 113 + logger.Error("error updating repo after commit", "error", err) 114 return helpers.ServerError(e, nil) 115 } 116
+34
server/handle_label_query_labels.go
···
··· 1 + package server 2 + 3 + import ( 4 + "github.com/labstack/echo/v4" 5 + ) 6 + 7 + type Label struct { 8 + Ver *int `json:"ver,omitempty"` 9 + Src string `json:"src"` 10 + Uri string `json:"uri"` 11 + Cid *string `json:"cid,omitempty"` 12 + Val string `json:"val"` 13 + Neg *bool `json:"neg,omitempty"` 14 + Cts string `json:"cts"` 15 + Exp *string `json:"exp,omitempty"` 16 + Sig []byte `json:"sig,omitempty"` 17 + } 18 + 19 + type ComAtprotoLabelQueryLabelsResponse struct { 20 + Cursor *string `json:"cursor,omitempty"` 21 + Labels []Label `json:"labels"` 22 + } 23 + 24 + func (s *Server) handleLabelQueryLabels(e echo.Context) error { 25 + svc := e.Request().Header.Get("atproto-proxy") 26 + if svc != "" || s.config.FallbackProxy != "" { 27 + return s.handleProxy(e) 28 + } 29 + 30 + return e.JSON(200, ComAtprotoLabelQueryLabelsResponse{ 31 + Cursor: nil, 32 + Labels: []Label{}, 33 + }) 34 + }
+105 -24
server/handle_oauth_authorize.go
··· 1 package server 2 3 import ( 4 "net/url" 5 "strings" 6 "time" ··· 8 "github.com/Azure/go-autorest/autorest/to" 9 "github.com/haileyok/cocoon/internal/helpers" 10 "github.com/haileyok/cocoon/oauth" 11 "github.com/haileyok/cocoon/oauth/provider" 12 "github.com/labstack/echo/v4" 13 ) 14 15 func (s *Server) handleOauthAuthorizeGet(e echo.Context) error { 16 - reqUri := e.QueryParam("request_uri") 17 - if reqUri == "" { 18 - // render page for logged out dev 19 - if s.config.Version == "dev" { 20 - return e.Render(200, "authorize.html", map[string]any{ 21 - "Scopes": []string{"atproto", "transition:generic"}, 22 - "AppName": "DEV MODE AUTHORIZATION PAGE", 23 - "Handle": "paula.cocoon.social", 24 - "RequestUri": "", 25 - }) 26 } 27 - return helpers.InputError(e, to.StringPtr("no request uri")) 28 } 29 30 repo, _, err := s.getSessionRepoOrErr(e) ··· 32 return e.Redirect(303, "/account/signin?"+e.QueryParams().Encode()) 33 } 34 35 - reqId, err := oauth.DecodeRequestUri(reqUri) 36 - if err != nil { 37 - return helpers.InputError(e, to.StringPtr(err.Error())) 38 - } 39 - 40 var req provider.OauthAuthorizationRequest 41 - if err := s.db.Raw("SELECT * FROM oauth_authorization_requests WHERE request_id = ?", nil, reqId).Scan(&req).Error; err != nil { 42 return helpers.ServerError(e, to.StringPtr(err.Error())) 43 } 44 ··· 58 data := map[string]any{ 59 "Scopes": scopes, 60 "AppName": appName, 61 - "RequestUri": reqUri, 62 "QueryParams": e.QueryParams().Encode(), 63 "Handle": repo.Actor.Handle, 64 } ··· 72 } 73 74 func (s *Server) handleOauthAuthorizePost(e echo.Context) error { 75 repo, _, err := s.getSessionRepoOrErr(e) 76 if err != nil { 77 return e.Redirect(303, "/account/signin") ··· 79 80 var req OauthAuthorizePostRequest 81 if err := e.Bind(&req); err != nil { 82 - s.logger.Error("error binding authorize post request", "error", err) 83 return helpers.InputError(e, nil) 84 } 85 ··· 89 } 90 91 var authReq provider.OauthAuthorizationRequest 92 - if err := s.db.Raw("SELECT * FROM oauth_authorization_requests WHERE request_id = ?", nil, reqId).Scan(&authReq).Error; err != nil { 93 return helpers.ServerError(e, to.StringPtr(err.Error())) 94 } 95 ··· 113 114 code := oauth.GenerateCode() 115 116 - if err := s.db.Exec("UPDATE oauth_authorization_requests SET sub = ?, code = ?, accepted = ?, ip = ? WHERE request_id = ?", nil, repo.Repo.Did, code, true, e.RealIP(), reqId).Error; err != nil { 117 - s.logger.Error("error updating authorization request", "error", err) 118 return helpers.ServerError(e, nil) 119 } 120 ··· 124 q.Set("code", code) 125 126 hashOrQuestion := "?" 127 - if authReq.ClientAuth.Method != "private_key_jwt" { 128 - hashOrQuestion = "#" 129 } 130 131 return e.Redirect(303, authReq.Parameters.RedirectURI+hashOrQuestion+q.Encode())
··· 1 package server 2 3 import ( 4 + "fmt" 5 "net/url" 6 "strings" 7 "time" ··· 9 "github.com/Azure/go-autorest/autorest/to" 10 "github.com/haileyok/cocoon/internal/helpers" 11 "github.com/haileyok/cocoon/oauth" 12 + "github.com/haileyok/cocoon/oauth/constants" 13 "github.com/haileyok/cocoon/oauth/provider" 14 "github.com/labstack/echo/v4" 15 ) 16 17 + type HandleOauthAuthorizeGetInput struct { 18 + RequestUri string `query:"request_uri"` 19 + } 20 + 21 func (s *Server) handleOauthAuthorizeGet(e echo.Context) error { 22 + ctx := e.Request().Context() 23 + 24 + logger := s.logger.With("name", "handleOauthAuthorizeGet") 25 + 26 + var input HandleOauthAuthorizeGetInput 27 + if err := e.Bind(&input); err != nil { 28 + logger.Error("error binding request", "err", err) 29 + return fmt.Errorf("error binding request") 30 + } 31 + 32 + var reqId string 33 + if input.RequestUri != "" { 34 + id, err := oauth.DecodeRequestUri(input.RequestUri) 35 + if err != nil { 36 + logger.Error("no request uri found in input", "url", e.Request().URL.String()) 37 + return helpers.InputError(e, to.StringPtr("no request uri")) 38 + } 39 + reqId = id 40 + } else { 41 + var parRequest provider.ParRequest 42 + if err := e.Bind(&parRequest); err != nil { 43 + s.logger.Error("error binding for standard auth request", "error", err) 44 + return helpers.InputError(e, to.StringPtr("InvalidRequest")) 45 + } 46 + 47 + if err := e.Validate(parRequest); err != nil { 48 + // render page for logged out dev 49 + if s.config.Version == "dev" && parRequest.ClientID == "" { 50 + return e.Render(200, "authorize.html", map[string]any{ 51 + "Scopes": []string{"atproto", "transition:generic"}, 52 + "AppName": "DEV MODE AUTHORIZATION PAGE", 53 + "Handle": "paula.cocoon.social", 54 + "RequestUri": "", 55 + }) 56 + } 57 + return helpers.InputError(e, to.StringPtr("no request uri and invalid parameters")) 58 + } 59 + 60 + client, clientAuth, err := s.oauthProvider.AuthenticateClient(ctx, parRequest.AuthenticateClientRequestBase, nil, &provider.AuthenticateClientOptions{ 61 + AllowMissingDpopProof: true, 62 + }) 63 + if err != nil { 64 + s.logger.Error("error authenticating client in standard request", "client_id", parRequest.ClientID, "error", err) 65 + return helpers.ServerError(e, to.StringPtr(err.Error())) 66 + } 67 + 68 + if parRequest.DpopJkt == nil { 69 + if client.Metadata.DpopBoundAccessTokens { 70 + } 71 + } else { 72 + if !client.Metadata.DpopBoundAccessTokens { 73 + msg := "dpop bound access tokens are not enabled for this client" 74 + return helpers.InputError(e, &msg) 75 + } 76 + } 77 + 78 + eat := time.Now().Add(constants.ParExpiresIn) 79 + id := oauth.GenerateRequestId() 80 + 81 + authRequest := &provider.OauthAuthorizationRequest{ 82 + RequestId: id, 83 + ClientId: client.Metadata.ClientID, 84 + ClientAuth: *clientAuth, 85 + Parameters: parRequest, 86 + ExpiresAt: eat, 87 } 88 + 89 + if err := s.db.Create(ctx, authRequest, nil).Error; err != nil { 90 + s.logger.Error("error creating auth request in db", "error", err) 91 + return helpers.ServerError(e, nil) 92 + } 93 + 94 + input.RequestUri = oauth.EncodeRequestUri(id) 95 + reqId = id 96 + 97 } 98 99 repo, _, err := s.getSessionRepoOrErr(e) ··· 101 return e.Redirect(303, "/account/signin?"+e.QueryParams().Encode()) 102 } 103 104 var req provider.OauthAuthorizationRequest 105 + if err := s.db.Raw(ctx, "SELECT * FROM oauth_authorization_requests WHERE request_id = ?", nil, reqId).Scan(&req).Error; err != nil { 106 return helpers.ServerError(e, to.StringPtr(err.Error())) 107 } 108 ··· 122 data := map[string]any{ 123 "Scopes": scopes, 124 "AppName": appName, 125 + "RequestUri": input.RequestUri, 126 "QueryParams": e.QueryParams().Encode(), 127 "Handle": repo.Actor.Handle, 128 } ··· 136 } 137 138 func (s *Server) handleOauthAuthorizePost(e echo.Context) error { 139 + ctx := e.Request().Context() 140 + logger := s.logger.With("name", "handleOauthAuthorizePost") 141 + 142 repo, _, err := s.getSessionRepoOrErr(e) 143 if err != nil { 144 return e.Redirect(303, "/account/signin") ··· 146 147 var req OauthAuthorizePostRequest 148 if err := e.Bind(&req); err != nil { 149 + logger.Error("error binding authorize post request", "error", err) 150 return helpers.InputError(e, nil) 151 } 152 ··· 156 } 157 158 var authReq provider.OauthAuthorizationRequest 159 + if err := s.db.Raw(ctx, "SELECT * FROM oauth_authorization_requests WHERE request_id = ?", nil, reqId).Scan(&authReq).Error; err != nil { 160 return helpers.ServerError(e, to.StringPtr(err.Error())) 161 } 162 ··· 180 181 code := oauth.GenerateCode() 182 183 + 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 { 184 + logger.Error("error updating authorization request", "error", err) 185 return helpers.ServerError(e, nil) 186 } 187 ··· 191 q.Set("code", code) 192 193 hashOrQuestion := "?" 194 + if authReq.Parameters.ResponseMode != nil { 195 + switch *authReq.Parameters.ResponseMode { 196 + case "fragment": 197 + hashOrQuestion = "#" 198 + case "query": 199 + // do nothing 200 + break 201 + default: 202 + if authReq.Parameters.ResponseType != "code" { 203 + hashOrQuestion = "#" 204 + } 205 + } 206 + } else { 207 + if authReq.Parameters.ResponseType != "code" { 208 + hashOrQuestion = "#" 209 + } 210 } 211 212 return e.Redirect(303, authReq.Parameters.RedirectURI+hashOrQuestion+q.Encode())
+17 -8
server/handle_oauth_par.go
··· 19 } 20 21 func (s *Server) handleOauthPar(e echo.Context) error { 22 var parRequest provider.ParRequest 23 if err := e.Bind(&parRequest); err != nil { 24 - s.logger.Error("error binding for par request", "error", err) 25 return helpers.ServerError(e, nil) 26 } 27 28 if err := e.Validate(parRequest); err != nil { 29 - s.logger.Error("missing parameters for par request", "error", err) 30 return helpers.InputError(e, nil) 31 } 32 ··· 34 dpopProof, err := s.oauthProvider.DpopManager.CheckProof(e.Request().Method, "https://"+s.config.Hostname+e.Request().URL.String(), e.Request().Header, nil) 35 if err != nil { 36 if errors.Is(err, dpop.ErrUseDpopNonce) { 37 return e.JSON(400, map[string]string{ 38 "error": "use_dpop_nonce", 39 }) 40 } 41 - s.logger.Error("error getting dpop proof", "error", err) 42 return helpers.InputError(e, nil) 43 } 44 ··· 48 AllowMissingDpopProof: true, 49 }) 50 if err != nil { 51 - s.logger.Error("error authenticating client", "client_id", parRequest.ClientID, "error", err) 52 return helpers.InputError(e, to.StringPtr(err.Error())) 53 } 54 ··· 59 } else { 60 if !client.Metadata.DpopBoundAccessTokens { 61 msg := "dpop bound access tokens are not enabled for this client" 62 - s.logger.Error(msg) 63 return helpers.InputError(e, &msg) 64 } 65 66 if dpopProof.JKT != *parRequest.DpopJkt { 67 msg := "supplied dpop jkt does not match header dpop jkt" 68 - s.logger.Error(msg) 69 return helpers.InputError(e, &msg) 70 } 71 } ··· 81 ExpiresAt: eat, 82 } 83 84 - if err := s.db.Create(authRequest, nil).Error; err != nil { 85 - s.logger.Error("error creating auth request in db", "error", err) 86 return helpers.ServerError(e, nil) 87 } 88
··· 19 } 20 21 func (s *Server) handleOauthPar(e echo.Context) error { 22 + ctx := e.Request().Context() 23 + logger := s.logger.With("name", "handleOauthPar") 24 + 25 var parRequest provider.ParRequest 26 if err := e.Bind(&parRequest); err != nil { 27 + logger.Error("error binding for par request", "error", err) 28 return helpers.ServerError(e, nil) 29 } 30 31 if err := e.Validate(parRequest); err != nil { 32 + logger.Error("missing parameters for par request", "error", err) 33 return helpers.InputError(e, nil) 34 } 35 ··· 37 dpopProof, err := s.oauthProvider.DpopManager.CheckProof(e.Request().Method, "https://"+s.config.Hostname+e.Request().URL.String(), e.Request().Header, nil) 38 if err != nil { 39 if errors.Is(err, dpop.ErrUseDpopNonce) { 40 + nonce := s.oauthProvider.NextNonce() 41 + if nonce != "" { 42 + e.Response().Header().Set("DPoP-Nonce", nonce) 43 + e.Response().Header().Add("access-control-expose-headers", "DPoP-Nonce") 44 + } 45 + logger.Error("nonce error: use_dpop_nonce", "headers", e.Request().Header) 46 return e.JSON(400, map[string]string{ 47 "error": "use_dpop_nonce", 48 }) 49 } 50 + logger.Error("error getting dpop proof", "error", err) 51 return helpers.InputError(e, nil) 52 } 53 ··· 57 AllowMissingDpopProof: true, 58 }) 59 if err != nil { 60 + logger.Error("error authenticating client", "client_id", parRequest.ClientID, "error", err) 61 return helpers.InputError(e, to.StringPtr(err.Error())) 62 } 63 ··· 68 } else { 69 if !client.Metadata.DpopBoundAccessTokens { 70 msg := "dpop bound access tokens are not enabled for this client" 71 + logger.Error(msg) 72 return helpers.InputError(e, &msg) 73 } 74 75 if dpopProof.JKT != *parRequest.DpopJkt { 76 msg := "supplied dpop jkt does not match header dpop jkt" 77 + logger.Error(msg) 78 return helpers.InputError(e, &msg) 79 } 80 } ··· 90 ExpiresAt: eat, 91 } 92 93 + if err := s.db.Create(ctx, authRequest, nil).Error; err != nil { 94 + logger.Error("error creating auth request in db", "error", err) 95 return helpers.ServerError(e, nil) 96 } 97
+21 -13
server/handle_oauth_token.go
··· 38 } 39 40 func (s *Server) handleOauthToken(e echo.Context) error { 41 var req OauthTokenRequest 42 if err := e.Bind(&req); err != nil { 43 - s.logger.Error("error binding token request", "error", err) 44 return helpers.ServerError(e, nil) 45 } 46 47 proof, err := s.oauthProvider.DpopManager.CheckProof(e.Request().Method, e.Request().URL.String(), e.Request().Header, nil) 48 if err != nil { 49 if errors.Is(err, dpop.ErrUseDpopNonce) { 50 return e.JSON(400, map[string]string{ 51 "error": "use_dpop_nonce", 52 }) 53 } 54 - s.logger.Error("error getting dpop proof", "error", err) 55 return helpers.InputError(e, nil) 56 } 57 ··· 59 AllowMissingDpopProof: true, 60 }) 61 if err != nil { 62 - s.logger.Error("error authenticating client", "client_id", req.ClientID, "error", err) 63 return helpers.InputError(e, to.StringPtr(err.Error())) 64 } 65 ··· 79 80 var authReq provider.OauthAuthorizationRequest 81 // get the lil guy and delete him 82 - if err := s.db.Raw("DELETE FROM oauth_authorization_requests WHERE code = ? RETURNING *", nil, *req.Code).Scan(&authReq).Error; err != nil { 83 - s.logger.Error("error finding authorization request", "error", err) 84 return helpers.ServerError(e, nil) 85 } 86 ··· 105 case "S256": 106 inputChal, err := base64.RawURLEncoding.DecodeString(*authReq.Parameters.CodeChallenge) 107 if err != nil { 108 - s.logger.Error("error decoding code challenge", "error", err) 109 return helpers.ServerError(e, nil) 110 } 111 ··· 123 return helpers.InputError(e, to.StringPtr("code_challenge parameter wasn't provided")) 124 } 125 126 - repo, err := s.getRepoActorByDid(*authReq.Sub) 127 if err != nil { 128 helpers.InputError(e, to.StringPtr("unable to find actor")) 129 } ··· 154 return err 155 } 156 157 - if err := s.db.Create(&provider.OauthToken{ 158 ClientId: authReq.ClientId, 159 ClientAuth: *clientAuth, 160 Parameters: authReq.Parameters, ··· 166 RefreshToken: refreshToken, 167 Ip: authReq.Ip, 168 }, nil).Error; err != nil { 169 - s.logger.Error("error creating token in db", "error", err) 170 return helpers.ServerError(e, nil) 171 } 172 ··· 194 } 195 196 var oauthToken provider.OauthToken 197 - if err := s.db.Raw("SELECT * FROM oauth_tokens WHERE refresh_token = ?", nil, req.RefreshToken).Scan(&oauthToken).Error; err != nil { 198 - s.logger.Error("error finding oauth token by refresh token", "error", err, "refresh_token", req.RefreshToken) 199 return helpers.ServerError(e, nil) 200 } 201 ··· 252 return err 253 } 254 255 - if err := s.db.Exec("UPDATE oauth_tokens SET token = ?, refresh_token = ?, expires_at = ?, updated_at = ? WHERE refresh_token = ?", nil, accessString, nextRefreshToken, eat, now, *req.RefreshToken).Error; err != nil { 256 - s.logger.Error("error updating token", "error", err) 257 return helpers.ServerError(e, nil) 258 } 259
··· 38 } 39 40 func (s *Server) handleOauthToken(e echo.Context) error { 41 + ctx := e.Request().Context() 42 + logger := s.logger.With("name", "handleOauthToken") 43 + 44 var req OauthTokenRequest 45 if err := e.Bind(&req); err != nil { 46 + logger.Error("error binding token request", "error", err) 47 return helpers.ServerError(e, nil) 48 } 49 50 proof, err := s.oauthProvider.DpopManager.CheckProof(e.Request().Method, e.Request().URL.String(), e.Request().Header, nil) 51 if err != nil { 52 if errors.Is(err, dpop.ErrUseDpopNonce) { 53 + nonce := s.oauthProvider.NextNonce() 54 + if nonce != "" { 55 + e.Response().Header().Set("DPoP-Nonce", nonce) 56 + e.Response().Header().Add("access-control-expose-headers", "DPoP-Nonce") 57 + } 58 return e.JSON(400, map[string]string{ 59 "error": "use_dpop_nonce", 60 }) 61 } 62 + logger.Error("error getting dpop proof", "error", err) 63 return helpers.InputError(e, nil) 64 } 65 ··· 67 AllowMissingDpopProof: true, 68 }) 69 if err != nil { 70 + logger.Error("error authenticating client", "client_id", req.ClientID, "error", err) 71 return helpers.InputError(e, to.StringPtr(err.Error())) 72 } 73 ··· 87 88 var authReq provider.OauthAuthorizationRequest 89 // get the lil guy and delete him 90 + if err := s.db.Raw(ctx, "DELETE FROM oauth_authorization_requests WHERE code = ? RETURNING *", nil, *req.Code).Scan(&authReq).Error; err != nil { 91 + logger.Error("error finding authorization request", "error", err) 92 return helpers.ServerError(e, nil) 93 } 94 ··· 113 case "S256": 114 inputChal, err := base64.RawURLEncoding.DecodeString(*authReq.Parameters.CodeChallenge) 115 if err != nil { 116 + logger.Error("error decoding code challenge", "error", err) 117 return helpers.ServerError(e, nil) 118 } 119 ··· 131 return helpers.InputError(e, to.StringPtr("code_challenge parameter wasn't provided")) 132 } 133 134 + repo, err := s.getRepoActorByDid(ctx, *authReq.Sub) 135 if err != nil { 136 helpers.InputError(e, to.StringPtr("unable to find actor")) 137 } ··· 162 return err 163 } 164 165 + if err := s.db.Create(ctx, &provider.OauthToken{ 166 ClientId: authReq.ClientId, 167 ClientAuth: *clientAuth, 168 Parameters: authReq.Parameters, ··· 174 RefreshToken: refreshToken, 175 Ip: authReq.Ip, 176 }, nil).Error; err != nil { 177 + logger.Error("error creating token in db", "error", err) 178 return helpers.ServerError(e, nil) 179 } 180 ··· 202 } 203 204 var oauthToken provider.OauthToken 205 + if err := s.db.Raw(ctx, "SELECT * FROM oauth_tokens WHERE refresh_token = ?", nil, req.RefreshToken).Scan(&oauthToken).Error; err != nil { 206 + logger.Error("error finding oauth token by refresh token", "error", err, "refresh_token", req.RefreshToken) 207 return helpers.ServerError(e, nil) 208 } 209 ··· 260 return err 261 } 262 263 + if err := s.db.Exec(ctx, "UPDATE oauth_tokens SET token = ?, refresh_token = ?, expires_at = ?, updated_at = ? WHERE refresh_token = ?", nil, accessString, nextRefreshToken, eat, now, *req.RefreshToken).Error; err != nil { 264 + logger.Error("error updating token", "error", err) 265 return helpers.ServerError(e, nil) 266 } 267
+6 -6
server/handle_proxy.go
··· 47 } 48 49 func (s *Server) handleProxy(e echo.Context) error { 50 - lgr := s.logger.With("handler", "handleProxy") 51 52 repo, isAuthed := e.Get("repo").(*models.RepoActor) 53 ··· 58 59 endpoint, svcDid, err := s.getAtprotoProxyEndpointFromRequest(e) 60 if err != nil { 61 - lgr.Error("could not get atproto proxy", "error", err) 62 return helpers.ServerError(e, nil) 63 } 64 ··· 90 } 91 hj, err := json.Marshal(header) 92 if err != nil { 93 - lgr.Error("error marshaling header", "error", err) 94 return helpers.ServerError(e, nil) 95 } 96 ··· 118 } 119 pj, err := json.Marshal(payload) 120 if err != nil { 121 - lgr.Error("error marashaling payload", "error", err) 122 return helpers.ServerError(e, nil) 123 } 124 ··· 129 130 sk, err := secp256k1secec.NewPrivateKey(repo.SigningKey) 131 if err != nil { 132 - lgr.Error("can't load private key", "error", err) 133 return err 134 } 135 136 R, S, _, err := sk.SignRaw(rand.Reader, hash[:]) 137 if err != nil { 138 - lgr.Error("error signing", "error", err) 139 } 140 141 rBytes := R.Bytes()
··· 47 } 48 49 func (s *Server) handleProxy(e echo.Context) error { 50 + logger := s.logger.With("handler", "handleProxy") 51 52 repo, isAuthed := e.Get("repo").(*models.RepoActor) 53 ··· 58 59 endpoint, svcDid, err := s.getAtprotoProxyEndpointFromRequest(e) 60 if err != nil { 61 + logger.Error("could not get atproto proxy", "error", err) 62 return helpers.ServerError(e, nil) 63 } 64 ··· 90 } 91 hj, err := json.Marshal(header) 92 if err != nil { 93 + logger.Error("error marshaling header", "error", err) 94 return helpers.ServerError(e, nil) 95 } 96 ··· 118 } 119 pj, err := json.Marshal(payload) 120 if err != nil { 121 + logger.Error("error marashaling payload", "error", err) 122 return helpers.ServerError(e, nil) 123 } 124 ··· 129 130 sk, err := secp256k1secec.NewPrivateKey(repo.SigningKey) 131 if err != nil { 132 + logger.Error("can't load private key", "error", err) 133 return err 134 } 135 136 R, S, _, err := sk.SignRaw(rand.Reader, hash[:]) 137 if err != nil { 138 + logger.Error("error signing", "error", err) 139 } 140 141 rBytes := R.Bytes()
+14 -11
server/handle_repo_apply_writes.go
··· 6 "github.com/labstack/echo/v4" 7 ) 8 9 - type ComAtprotoRepoApplyWritesRequest struct { 10 Repo string `json:"repo" validate:"required,atproto-did"` 11 Validate *bool `json:"bool,omitempty"` 12 Writes []ComAtprotoRepoApplyWritesItem `json:"writes"` ··· 20 Value *MarshalableMap `json:"value,omitempty"` 21 } 22 23 - type ComAtprotoRepoApplyWritesResponse struct { 24 Commit RepoCommit `json:"commit"` 25 Results []ApplyWriteResult `json:"results"` 26 } 27 28 func (s *Server) handleApplyWrites(e echo.Context) error { 29 - repo := e.Get("repo").(*models.RepoActor) 30 31 - var req ComAtprotoRepoApplyWritesRequest 32 if err := e.Bind(&req); err != nil { 33 - s.logger.Error("error binding", "error", err) 34 return helpers.ServerError(e, nil) 35 } 36 37 if err := e.Validate(req); err != nil { 38 - s.logger.Error("error validating", "error", err) 39 return helpers.InputError(e, nil) 40 } 41 42 if repo.Repo.Did != req.Repo { 43 - s.logger.Warn("mismatched repo/auth") 44 return helpers.InputError(e, nil) 45 } 46 47 - ops := []Op{} 48 for _, item := range req.Writes { 49 ops = append(ops, Op{ 50 Type: OpType(item.Type), ··· 54 }) 55 } 56 57 - results, err := s.repoman.applyWrites(repo.Repo, ops, req.SwapCommit) 58 if err != nil { 59 - s.logger.Error("error applying writes", "error", err) 60 return helpers.ServerError(e, nil) 61 } 62 ··· 66 results[i].Commit = nil 67 } 68 69 - return e.JSON(200, ComAtprotoRepoApplyWritesResponse{ 70 Commit: commit, 71 Results: results, 72 })
··· 6 "github.com/labstack/echo/v4" 7 ) 8 9 + type ComAtprotoRepoApplyWritesInput struct { 10 Repo string `json:"repo" validate:"required,atproto-did"` 11 Validate *bool `json:"bool,omitempty"` 12 Writes []ComAtprotoRepoApplyWritesItem `json:"writes"` ··· 20 Value *MarshalableMap `json:"value,omitempty"` 21 } 22 23 + type ComAtprotoRepoApplyWritesOutput struct { 24 Commit RepoCommit `json:"commit"` 25 Results []ApplyWriteResult `json:"results"` 26 } 27 28 func (s *Server) handleApplyWrites(e echo.Context) error { 29 + ctx := e.Request().Context() 30 + logger := s.logger.With("name", "handleRepoApplyWrites") 31 32 + var req ComAtprotoRepoApplyWritesInput 33 if err := e.Bind(&req); err != nil { 34 + logger.Error("error binding", "error", err) 35 return helpers.ServerError(e, nil) 36 } 37 38 if err := e.Validate(req); err != nil { 39 + logger.Error("error validating", "error", err) 40 return helpers.InputError(e, nil) 41 } 42 43 + repo := e.Get("repo").(*models.RepoActor) 44 + 45 if repo.Repo.Did != req.Repo { 46 + logger.Warn("mismatched repo/auth") 47 return helpers.InputError(e, nil) 48 } 49 50 + ops := make([]Op, 0, len(req.Writes)) 51 for _, item := range req.Writes { 52 ops = append(ops, Op{ 53 Type: OpType(item.Type), ··· 57 }) 58 } 59 60 + results, err := s.repoman.applyWrites(ctx, repo.Repo, ops, req.SwapCommit) 61 if err != nil { 62 + logger.Error("error applying writes", "error", err) 63 return helpers.ServerError(e, nil) 64 } 65 ··· 69 results[i].Commit = nil 70 } 71 72 + return e.JSON(200, ComAtprotoRepoApplyWritesOutput{ 73 Commit: commit, 74 Results: results, 75 })
+10 -7
server/handle_repo_create_record.go
··· 6 "github.com/labstack/echo/v4" 7 ) 8 9 - type ComAtprotoRepoCreateRecordRequest struct { 10 Repo string `json:"repo" validate:"required,atproto-did"` 11 Collection string `json:"collection" validate:"required,atproto-nsid"` 12 Rkey *string `json:"rkey,omitempty"` ··· 17 } 18 19 func (s *Server) handleCreateRecord(e echo.Context) error { 20 repo := e.Get("repo").(*models.RepoActor) 21 22 - var req ComAtprotoRepoCreateRecordRequest 23 if err := e.Bind(&req); err != nil { 24 - s.logger.Error("error binding", "error", err) 25 return helpers.ServerError(e, nil) 26 } 27 28 if err := e.Validate(req); err != nil { 29 - s.logger.Error("error validating", "error", err) 30 return helpers.InputError(e, nil) 31 } 32 33 if repo.Repo.Did != req.Repo { 34 - s.logger.Warn("mismatched repo/auth") 35 return helpers.InputError(e, nil) 36 } 37 ··· 40 optype = OpTypeUpdate 41 } 42 43 - results, err := s.repoman.applyWrites(repo.Repo, []Op{ 44 { 45 Type: optype, 46 Collection: req.Collection, ··· 51 }, 52 }, req.SwapCommit) 53 if err != nil { 54 - s.logger.Error("error applying writes", "error", err) 55 return helpers.ServerError(e, nil) 56 } 57
··· 6 "github.com/labstack/echo/v4" 7 ) 8 9 + type ComAtprotoRepoCreateRecordInput struct { 10 Repo string `json:"repo" validate:"required,atproto-did"` 11 Collection string `json:"collection" validate:"required,atproto-nsid"` 12 Rkey *string `json:"rkey,omitempty"` ··· 17 } 18 19 func (s *Server) handleCreateRecord(e echo.Context) error { 20 + ctx := e.Request().Context() 21 + logger := s.logger.With("name", "handleCreateRecord") 22 + 23 repo := e.Get("repo").(*models.RepoActor) 24 25 + var req ComAtprotoRepoCreateRecordInput 26 if err := e.Bind(&req); err != nil { 27 + logger.Error("error binding", "error", err) 28 return helpers.ServerError(e, nil) 29 } 30 31 if err := e.Validate(req); err != nil { 32 + logger.Error("error validating", "error", err) 33 return helpers.InputError(e, nil) 34 } 35 36 if repo.Repo.Did != req.Repo { 37 + logger.Warn("mismatched repo/auth") 38 return helpers.InputError(e, nil) 39 } 40 ··· 43 optype = OpTypeUpdate 44 } 45 46 + results, err := s.repoman.applyWrites(ctx, repo.Repo, []Op{ 47 { 48 Type: optype, 49 Collection: req.Collection, ··· 54 }, 55 }, req.SwapCommit) 56 if err != nil { 57 + logger.Error("error applying writes", "error", err) 58 return helpers.ServerError(e, nil) 59 } 60
+10 -7
server/handle_repo_delete_record.go
··· 6 "github.com/labstack/echo/v4" 7 ) 8 9 - type ComAtprotoRepoDeleteRecordRequest struct { 10 Repo string `json:"repo" validate:"required,atproto-did"` 11 Collection string `json:"collection" validate:"required,atproto-nsid"` 12 Rkey string `json:"rkey" validate:"required,atproto-rkey"` ··· 15 } 16 17 func (s *Server) handleDeleteRecord(e echo.Context) error { 18 repo := e.Get("repo").(*models.RepoActor) 19 20 - var req ComAtprotoRepoDeleteRecordRequest 21 if err := e.Bind(&req); err != nil { 22 - s.logger.Error("error binding", "error", err) 23 return helpers.ServerError(e, nil) 24 } 25 26 if err := e.Validate(req); err != nil { 27 - s.logger.Error("error validating", "error", err) 28 return helpers.InputError(e, nil) 29 } 30 31 if repo.Repo.Did != req.Repo { 32 - s.logger.Warn("mismatched repo/auth") 33 return helpers.InputError(e, nil) 34 } 35 36 - results, err := s.repoman.applyWrites(repo.Repo, []Op{ 37 { 38 Type: OpTypeDelete, 39 Collection: req.Collection, ··· 42 }, 43 }, req.SwapCommit) 44 if err != nil { 45 - s.logger.Error("error applying writes", "error", err) 46 return helpers.ServerError(e, nil) 47 } 48
··· 6 "github.com/labstack/echo/v4" 7 ) 8 9 + type ComAtprotoRepoDeleteRecordInput struct { 10 Repo string `json:"repo" validate:"required,atproto-did"` 11 Collection string `json:"collection" validate:"required,atproto-nsid"` 12 Rkey string `json:"rkey" validate:"required,atproto-rkey"` ··· 15 } 16 17 func (s *Server) handleDeleteRecord(e echo.Context) error { 18 + ctx := e.Request().Context() 19 + logger := s.logger.With("name", "handleDeleteRecord") 20 + 21 repo := e.Get("repo").(*models.RepoActor) 22 23 + var req ComAtprotoRepoDeleteRecordInput 24 if err := e.Bind(&req); err != nil { 25 + logger.Error("error binding", "error", err) 26 return helpers.ServerError(e, nil) 27 } 28 29 if err := e.Validate(req); err != nil { 30 + logger.Error("error validating", "error", err) 31 return helpers.InputError(e, nil) 32 } 33 34 if repo.Repo.Did != req.Repo { 35 + logger.Warn("mismatched repo/auth") 36 return helpers.InputError(e, nil) 37 } 38 39 + results, err := s.repoman.applyWrites(ctx, repo.Repo, []Op{ 40 { 41 Type: OpTypeDelete, 42 Collection: req.Collection, ··· 45 }, 46 }, req.SwapCommit) 47 if err != nil { 48 + logger.Error("error applying writes", "error", err) 49 return helpers.ServerError(e, nil) 50 } 51
+8 -5
server/handle_repo_describe_repo.go
··· 20 } 21 22 func (s *Server) handleDescribeRepo(e echo.Context) error { 23 did := e.QueryParam("repo") 24 - repo, err := s.getRepoActorByDid(did) 25 if err != nil { 26 if err == gorm.ErrRecordNotFound { 27 return helpers.InputError(e, to.StringPtr("RepoNotFound")) 28 } 29 30 - s.logger.Error("error looking up repo", "error", err) 31 return helpers.ServerError(e, nil) 32 } 33 ··· 35 36 diddoc, err := s.passport.FetchDoc(e.Request().Context(), repo.Repo.Did) 37 if err != nil { 38 - s.logger.Error("error fetching diddoc", "error", err) 39 return helpers.ServerError(e, nil) 40 } 41 ··· 64 } 65 66 var records []models.Record 67 - if err := s.db.Raw("SELECT DISTINCT(nsid) FROM records WHERE did = ?", nil, repo.Repo.Did).Scan(&records).Error; err != nil { 68 - s.logger.Error("error getting collections", "error", err) 69 return helpers.ServerError(e, nil) 70 } 71
··· 20 } 21 22 func (s *Server) handleDescribeRepo(e echo.Context) error { 23 + ctx := e.Request().Context() 24 + logger := s.logger.With("name", "handleDescribeRepo") 25 + 26 did := e.QueryParam("repo") 27 + repo, err := s.getRepoActorByDid(ctx, did) 28 if err != nil { 29 if err == gorm.ErrRecordNotFound { 30 return helpers.InputError(e, to.StringPtr("RepoNotFound")) 31 } 32 33 + logger.Error("error looking up repo", "error", err) 34 return helpers.ServerError(e, nil) 35 } 36 ··· 38 39 diddoc, err := s.passport.FetchDoc(e.Request().Context(), repo.Repo.Did) 40 if err != nil { 41 + logger.Error("error fetching diddoc", "error", err) 42 return helpers.ServerError(e, nil) 43 } 44 ··· 67 } 68 69 var records []models.Record 70 + if err := s.db.Raw(ctx, "SELECT DISTINCT(nsid) FROM records WHERE did = ?", nil, repo.Repo.Did).Scan(&records).Error; err != nil { 71 + logger.Error("error getting collections", "error", err) 72 return helpers.ServerError(e, nil) 73 } 74
+3 -1
server/handle_repo_get_record.go
··· 14 } 15 16 func (s *Server) handleRepoGetRecord(e echo.Context) error { 17 repo := e.QueryParam("repo") 18 collection := e.QueryParam("collection") 19 rkey := e.QueryParam("rkey") ··· 32 } 33 34 var record models.Record 35 - if err := s.db.Raw("SELECT * FROM records WHERE did = ? AND nsid = ? AND rkey = ?"+cidquery, nil, params...).Scan(&record).Error; err != nil { 36 // TODO: handle error nicely 37 return err 38 }
··· 14 } 15 16 func (s *Server) handleRepoGetRecord(e echo.Context) error { 17 + ctx := e.Request().Context() 18 + 19 repo := e.QueryParam("repo") 20 collection := e.QueryParam("collection") 21 rkey := e.QueryParam("rkey") ··· 34 } 35 36 var record models.Record 37 + if err := s.db.Raw(ctx, "SELECT * FROM records WHERE did = ? AND nsid = ? AND rkey = ?"+cidquery, nil, params...).Scan(&record).Error; err != nil { 38 // TODO: handle error nicely 39 return err 40 }
+115
server/handle_repo_list_missing_blobs.go
···
··· 1 + package server 2 + 3 + import ( 4 + "fmt" 5 + "strconv" 6 + 7 + "github.com/bluesky-social/indigo/atproto/atdata" 8 + "github.com/haileyok/cocoon/internal/helpers" 9 + "github.com/haileyok/cocoon/models" 10 + "github.com/ipfs/go-cid" 11 + "github.com/labstack/echo/v4" 12 + ) 13 + 14 + type ComAtprotoRepoListMissingBlobsResponse struct { 15 + Cursor *string `json:"cursor,omitempty"` 16 + Blobs []ComAtprotoRepoListMissingBlobsRecordBlob `json:"blobs"` 17 + } 18 + 19 + type ComAtprotoRepoListMissingBlobsRecordBlob struct { 20 + Cid string `json:"cid"` 21 + RecordUri string `json:"recordUri"` 22 + } 23 + 24 + func (s *Server) handleListMissingBlobs(e echo.Context) error { 25 + ctx := e.Request().Context() 26 + logger := s.logger.With("name", "handleListMissingBlos") 27 + 28 + urepo := e.Get("repo").(*models.RepoActor) 29 + 30 + limitStr := e.QueryParam("limit") 31 + cursor := e.QueryParam("cursor") 32 + 33 + limit := 500 34 + if limitStr != "" { 35 + if l, err := strconv.Atoi(limitStr); err == nil && l > 0 && l <= 1000 { 36 + limit = l 37 + } 38 + } 39 + 40 + var records []models.Record 41 + if err := s.db.Raw(ctx, "SELECT * FROM records WHERE did = ?", nil, urepo.Repo.Did).Scan(&records).Error; err != nil { 42 + logger.Error("failed to get records for listMissingBlobs", "error", err) 43 + return helpers.ServerError(e, nil) 44 + } 45 + 46 + type blobRef struct { 47 + cid cid.Cid 48 + recordUri string 49 + } 50 + var allBlobRefs []blobRef 51 + 52 + for _, rec := range records { 53 + blobs := getBlobsFromRecord(rec.Value) 54 + recordUri := fmt.Sprintf("at://%s/%s/%s", urepo.Repo.Did, rec.Nsid, rec.Rkey) 55 + for _, b := range blobs { 56 + allBlobRefs = append(allBlobRefs, blobRef{cid: cid.Cid(b.Ref), recordUri: recordUri}) 57 + } 58 + } 59 + 60 + missingBlobs := make([]ComAtprotoRepoListMissingBlobsRecordBlob, 0) 61 + seenCids := make(map[string]bool) 62 + 63 + for _, ref := range allBlobRefs { 64 + cidStr := ref.cid.String() 65 + 66 + if seenCids[cidStr] { 67 + continue 68 + } 69 + 70 + if cursor != "" && cidStr <= cursor { 71 + continue 72 + } 73 + 74 + var count int64 75 + if err := s.db.Raw(ctx, "SELECT COUNT(*) FROM blobs WHERE did = ? AND cid = ?", nil, urepo.Repo.Did, ref.cid.Bytes()).Scan(&count).Error; err != nil { 76 + continue 77 + } 78 + 79 + if count == 0 { 80 + missingBlobs = append(missingBlobs, ComAtprotoRepoListMissingBlobsRecordBlob{ 81 + Cid: cidStr, 82 + RecordUri: ref.recordUri, 83 + }) 84 + seenCids[cidStr] = true 85 + 86 + if len(missingBlobs) >= limit { 87 + break 88 + } 89 + } 90 + } 91 + 92 + var nextCursor *string 93 + if len(missingBlobs) > 0 && len(missingBlobs) >= limit { 94 + lastCid := missingBlobs[len(missingBlobs)-1].Cid 95 + nextCursor = &lastCid 96 + } 97 + 98 + return e.JSON(200, ComAtprotoRepoListMissingBlobsResponse{ 99 + Cursor: nextCursor, 100 + Blobs: missingBlobs, 101 + }) 102 + } 103 + 104 + func getBlobsFromRecord(data []byte) []atdata.Blob { 105 + if len(data) == 0 { 106 + return nil 107 + } 108 + 109 + decoded, err := atdata.UnmarshalCBOR(data) 110 + if err != nil { 111 + return nil 112 + } 113 + 114 + return atdata.ExtractBlobs(decoded) 115 + }
+7 -4
server/handle_repo_list_records.go
··· 46 } 47 48 func (s *Server) handleListRecords(e echo.Context) error { 49 var req ComAtprotoRepoListRecordsRequest 50 if err := e.Bind(&req); err != nil { 51 - s.logger.Error("could not bind list records request", "error", err) 52 return helpers.ServerError(e, nil) 53 } 54 ··· 78 79 did := req.Repo 80 if _, err := syntax.ParseDID(did); err != nil { 81 - actor, err := s.getActorByHandle(req.Repo) 82 if err != nil { 83 return helpers.InputError(e, to.StringPtr("RepoNotFound")) 84 } ··· 93 params = append(params, limit) 94 95 var records []models.Record 96 - if err := s.db.Raw("SELECT * FROM records WHERE did = ? AND nsid = ? "+cursorquery+" ORDER BY created_at "+sort+" limit ?", nil, params...).Scan(&records).Error; err != nil { 97 - s.logger.Error("error getting records", "error", err) 98 return helpers.ServerError(e, nil) 99 } 100
··· 46 } 47 48 func (s *Server) handleListRecords(e echo.Context) error { 49 + ctx := e.Request().Context() 50 + logger := s.logger.With("name", "handleListRecords") 51 + 52 var req ComAtprotoRepoListRecordsRequest 53 if err := e.Bind(&req); err != nil { 54 + logger.Error("could not bind list records request", "error", err) 55 return helpers.ServerError(e, nil) 56 } 57 ··· 81 82 did := req.Repo 83 if _, err := syntax.ParseDID(did); err != nil { 84 + actor, err := s.getActorByHandle(ctx, req.Repo) 85 if err != nil { 86 return helpers.InputError(e, to.StringPtr("RepoNotFound")) 87 } ··· 96 params = append(params, limit) 97 98 var records []models.Record 99 + if err := s.db.Raw(ctx, "SELECT * FROM records WHERE did = ? AND nsid = ? "+cursorquery+" ORDER BY created_at "+sort+" limit ?", nil, params...).Scan(&records).Error; err != nil { 100 + logger.Error("error getting records", "error", err) 101 return helpers.ServerError(e, nil) 102 } 103
+3 -1
server/handle_repo_list_repos.go
··· 21 22 // TODO: paginate this bitch 23 func (s *Server) handleListRepos(e echo.Context) error { 24 var repos []models.Repo 25 - if err := s.db.Raw("SELECT * FROM repos ORDER BY created_at DESC LIMIT 500", nil).Scan(&repos).Error; err != nil { 26 return err 27 } 28
··· 21 22 // TODO: paginate this bitch 23 func (s *Server) handleListRepos(e echo.Context) error { 24 + ctx := e.Request().Context() 25 + 26 var repos []models.Repo 27 + if err := s.db.Raw(ctx, "SELECT * FROM repos ORDER BY created_at DESC LIMIT 500", nil).Scan(&repos).Error; err != nil { 28 return err 29 } 30
+10 -7
server/handle_repo_put_record.go
··· 6 "github.com/labstack/echo/v4" 7 ) 8 9 - type ComAtprotoRepoPutRecordRequest struct { 10 Repo string `json:"repo" validate:"required,atproto-did"` 11 Collection string `json:"collection" validate:"required,atproto-nsid"` 12 Rkey string `json:"rkey" validate:"required,atproto-rkey"` ··· 17 } 18 19 func (s *Server) handlePutRecord(e echo.Context) error { 20 repo := e.Get("repo").(*models.RepoActor) 21 22 - var req ComAtprotoRepoPutRecordRequest 23 if err := e.Bind(&req); err != nil { 24 - s.logger.Error("error binding", "error", err) 25 return helpers.ServerError(e, nil) 26 } 27 28 if err := e.Validate(req); err != nil { 29 - s.logger.Error("error validating", "error", err) 30 return helpers.InputError(e, nil) 31 } 32 33 if repo.Repo.Did != req.Repo { 34 - s.logger.Warn("mismatched repo/auth") 35 return helpers.InputError(e, nil) 36 } 37 ··· 40 optype = OpTypeUpdate 41 } 42 43 - results, err := s.repoman.applyWrites(repo.Repo, []Op{ 44 { 45 Type: optype, 46 Collection: req.Collection, ··· 51 }, 52 }, req.SwapCommit) 53 if err != nil { 54 - s.logger.Error("error applying writes", "error", err) 55 return helpers.ServerError(e, nil) 56 } 57
··· 6 "github.com/labstack/echo/v4" 7 ) 8 9 + type ComAtprotoRepoPutRecordInput struct { 10 Repo string `json:"repo" validate:"required,atproto-did"` 11 Collection string `json:"collection" validate:"required,atproto-nsid"` 12 Rkey string `json:"rkey" validate:"required,atproto-rkey"` ··· 17 } 18 19 func (s *Server) handlePutRecord(e echo.Context) error { 20 + ctx := e.Request().Context() 21 + logger := s.logger.With("name", "handlePutRecord") 22 + 23 repo := e.Get("repo").(*models.RepoActor) 24 25 + var req ComAtprotoRepoPutRecordInput 26 if err := e.Bind(&req); err != nil { 27 + logger.Error("error binding", "error", err) 28 return helpers.ServerError(e, nil) 29 } 30 31 if err := e.Validate(req); err != nil { 32 + logger.Error("error validating", "error", err) 33 return helpers.InputError(e, nil) 34 } 35 36 if repo.Repo.Did != req.Repo { 37 + logger.Warn("mismatched repo/auth") 38 return helpers.InputError(e, nil) 39 } 40 ··· 43 optype = OpTypeUpdate 44 } 45 46 + results, err := s.repoman.applyWrites(ctx, repo.Repo, []Op{ 47 { 48 Type: optype, 49 Collection: req.Collection, ··· 54 }, 55 }, req.SwapCommit) 56 if err != nil { 57 + logger.Error("error applying writes", "error", err) 58 return helpers.ServerError(e, nil) 59 } 60
+13 -10
server/handle_repo_upload_blob.go
··· 32 } 33 34 func (s *Server) handleRepoUploadBlob(e echo.Context) error { 35 urepo := e.Get("repo").(*models.RepoActor) 36 37 mime := e.Request().Header.Get("content-type") ··· 51 Storage: storage, 52 } 53 54 - if err := s.db.Create(&blob, nil).Error; err != nil { 55 - s.logger.Error("error creating new blob in db", "error", err) 56 return helpers.ServerError(e, nil) 57 } 58 ··· 69 break 70 } 71 } else if err != nil && err != io.ErrUnexpectedEOF { 72 - s.logger.Error("error reading blob", "error", err) 73 return helpers.ServerError(e, nil) 74 } 75 ··· 84 Data: data, 85 } 86 87 - if err := s.db.Create(&blobPart, nil).Error; err != nil { 88 - s.logger.Error("error adding blob part to db", "error", err) 89 return helpers.ServerError(e, nil) 90 } 91 } ··· 98 99 c, err := cid.NewPrefixV1(cid.Raw, multihash.SHA2_256).Sum(fulldata.Bytes()) 100 if err != nil { 101 - s.logger.Error("error creating cid prefix", "error", err) 102 return helpers.ServerError(e, nil) 103 } 104 ··· 115 116 sess, err := session.NewSession(config) 117 if err != nil { 118 - s.logger.Error("error creating aws session", "error", err) 119 return helpers.ServerError(e, nil) 120 } 121 ··· 126 Key: aws.String(fmt.Sprintf("blobs/%s/%s", urepo.Repo.Did, c.String())), 127 Body: bytes.NewReader(fulldata.Bytes()), 128 }); err != nil { 129 - s.logger.Error("error uploading blob to s3", "error", err) 130 return helpers.ServerError(e, nil) 131 } 132 } 133 134 - if err := s.db.Exec("UPDATE blobs SET cid = ? WHERE id = ?", nil, c.Bytes(), blob.ID).Error; err != nil { 135 // there should probably be somme handling here if this fails... 136 - s.logger.Error("error updating blob", "error", err) 137 return helpers.ServerError(e, nil) 138 } 139
··· 32 } 33 34 func (s *Server) handleRepoUploadBlob(e echo.Context) error { 35 + ctx := e.Request().Context() 36 + logger := s.logger.With("name", "handleRepoUploadBlob") 37 + 38 urepo := e.Get("repo").(*models.RepoActor) 39 40 mime := e.Request().Header.Get("content-type") ··· 54 Storage: storage, 55 } 56 57 + if err := s.db.Create(ctx, &blob, nil).Error; err != nil { 58 + logger.Error("error creating new blob in db", "error", err) 59 return helpers.ServerError(e, nil) 60 } 61 ··· 72 break 73 } 74 } else if err != nil && err != io.ErrUnexpectedEOF { 75 + logger.Error("error reading blob", "error", err) 76 return helpers.ServerError(e, nil) 77 } 78 ··· 87 Data: data, 88 } 89 90 + if err := s.db.Create(ctx, &blobPart, nil).Error; err != nil { 91 + logger.Error("error adding blob part to db", "error", err) 92 return helpers.ServerError(e, nil) 93 } 94 } ··· 101 102 c, err := cid.NewPrefixV1(cid.Raw, multihash.SHA2_256).Sum(fulldata.Bytes()) 103 if err != nil { 104 + logger.Error("error creating cid prefix", "error", err) 105 return helpers.ServerError(e, nil) 106 } 107 ··· 118 119 sess, err := session.NewSession(config) 120 if err != nil { 121 + logger.Error("error creating aws session", "error", err) 122 return helpers.ServerError(e, nil) 123 } 124 ··· 129 Key: aws.String(fmt.Sprintf("blobs/%s/%s", urepo.Repo.Did, c.String())), 130 Body: bytes.NewReader(fulldata.Bytes()), 131 }); err != nil { 132 + logger.Error("error uploading blob to s3", "error", err) 133 return helpers.ServerError(e, nil) 134 } 135 } 136 137 + if err := s.db.Exec(ctx, "UPDATE blobs SET cid = ? WHERE id = ?", nil, c.Bytes(), blob.ID).Error; err != nil { 138 // there should probably be somme handling here if this fails... 139 + logger.Error("error updating blob", "error", err) 140 return helpers.ServerError(e, nil) 141 } 142
+6 -3
server/handle_server_activate_account.go
··· 18 } 19 20 func (s *Server) handleServerActivateAccount(e echo.Context) error { 21 var req ComAtprotoServerDeactivateAccountRequest 22 if err := e.Bind(&req); err != nil { 23 - s.logger.Error("error binding", "error", err) 24 return helpers.ServerError(e, nil) 25 } 26 27 urepo := e.Get("repo").(*models.RepoActor) 28 29 - if err := s.db.Exec("UPDATE repos SET deactivated = ? WHERE did = ?", nil, false, urepo.Repo.Did).Error; err != nil { 30 - s.logger.Error("error updating account status to deactivated", "error", err) 31 return helpers.ServerError(e, nil) 32 } 33
··· 18 } 19 20 func (s *Server) handleServerActivateAccount(e echo.Context) error { 21 + ctx := e.Request().Context() 22 + logger := s.logger.With("name", "handleServerActivateAccount") 23 + 24 var req ComAtprotoServerDeactivateAccountRequest 25 if err := e.Bind(&req); err != nil { 26 + logger.Error("error binding", "error", err) 27 return helpers.ServerError(e, nil) 28 } 29 30 urepo := e.Get("repo").(*models.RepoActor) 31 32 + if err := s.db.Exec(ctx, "UPDATE repos SET deactivated = ? WHERE did = ?", nil, false, urepo.Repo.Did).Error; err != nil { 33 + logger.Error("error updating account status to deactivated", "error", err) 34 return helpers.ServerError(e, nil) 35 } 36
+10 -7
server/handle_server_check_account_status.go
··· 20 } 21 22 func (s *Server) handleServerCheckAccountStatus(e echo.Context) error { 23 urepo := e.Get("repo").(*models.RepoActor) 24 25 resp := ComAtprotoServerCheckAccountStatusResponse{ ··· 31 32 rootcid, err := cid.Cast(urepo.Root) 33 if err != nil { 34 - s.logger.Error("error casting cid", "error", err) 35 return helpers.ServerError(e, nil) 36 } 37 resp.RepoCommit = rootcid.String() ··· 41 } 42 43 var blockCtResp CountResp 44 - if err := s.db.Raw("SELECT COUNT(*) AS ct FROM blocks WHERE did = ?", nil, urepo.Repo.Did).Scan(&blockCtResp).Error; err != nil { 45 - s.logger.Error("error getting block count", "error", err) 46 return helpers.ServerError(e, nil) 47 } 48 resp.RepoBlocks = blockCtResp.Ct 49 50 var recCtResp CountResp 51 - if err := s.db.Raw("SELECT COUNT(*) AS ct FROM records WHERE did = ?", nil, urepo.Repo.Did).Scan(&recCtResp).Error; err != nil { 52 - s.logger.Error("error getting record count", "error", err) 53 return helpers.ServerError(e, nil) 54 } 55 resp.IndexedRecords = recCtResp.Ct 56 57 var blobCtResp CountResp 58 - if err := s.db.Raw("SELECT COUNT(*) AS ct FROM blobs WHERE did = ?", nil, urepo.Repo.Did).Scan(&blobCtResp).Error; err != nil { 59 - s.logger.Error("error getting record count", "error", err) 60 return helpers.ServerError(e, nil) 61 } 62 resp.ExpectedBlobs = blobCtResp.Ct
··· 20 } 21 22 func (s *Server) handleServerCheckAccountStatus(e echo.Context) error { 23 + ctx := e.Request().Context() 24 + logger := s.logger.With("name", "handleServerCheckAccountStatus") 25 + 26 urepo := e.Get("repo").(*models.RepoActor) 27 28 resp := ComAtprotoServerCheckAccountStatusResponse{ ··· 34 35 rootcid, err := cid.Cast(urepo.Root) 36 if err != nil { 37 + logger.Error("error casting cid", "error", err) 38 return helpers.ServerError(e, nil) 39 } 40 resp.RepoCommit = rootcid.String() ··· 44 } 45 46 var blockCtResp CountResp 47 + if err := s.db.Raw(ctx, "SELECT COUNT(*) AS ct FROM blocks WHERE did = ?", nil, urepo.Repo.Did).Scan(&blockCtResp).Error; err != nil { 48 + logger.Error("error getting block count", "error", err) 49 return helpers.ServerError(e, nil) 50 } 51 resp.RepoBlocks = blockCtResp.Ct 52 53 var recCtResp CountResp 54 + if err := s.db.Raw(ctx, "SELECT COUNT(*) AS ct FROM records WHERE did = ?", nil, urepo.Repo.Did).Scan(&recCtResp).Error; err != nil { 55 + logger.Error("error getting record count", "error", err) 56 return helpers.ServerError(e, nil) 57 } 58 resp.IndexedRecords = recCtResp.Ct 59 60 var blobCtResp CountResp 61 + if err := s.db.Raw(ctx, "SELECT COUNT(*) AS ct FROM blobs WHERE did = ?", nil, urepo.Repo.Did).Scan(&blobCtResp).Error; err != nil { 62 + logger.Error("error getting record count", "error", err) 63 return helpers.ServerError(e, nil) 64 } 65 resp.ExpectedBlobs = blobCtResp.Ct
+6 -3
server/handle_server_confirm_email.go
··· 15 } 16 17 func (s *Server) handleServerConfirmEmail(e echo.Context) error { 18 urepo := e.Get("repo").(*models.RepoActor) 19 20 var req ComAtprotoServerConfirmEmailRequest 21 if err := e.Bind(&req); err != nil { 22 - s.logger.Error("error binding", "error", err) 23 return helpers.ServerError(e, nil) 24 } 25 ··· 41 42 now := time.Now().UTC() 43 44 - if err := s.db.Exec("UPDATE repos SET email_verification_code = NULL, email_verification_code_expires_at = NULL, email_confirmed_at = ? WHERE did = ?", nil, now, urepo.Repo.Did).Error; err != nil { 45 - s.logger.Error("error updating user", "error", err) 46 return helpers.ServerError(e, nil) 47 } 48
··· 15 } 16 17 func (s *Server) handleServerConfirmEmail(e echo.Context) error { 18 + ctx := e.Request().Context() 19 + logger := s.logger.With("name", "handleServerConfirmEmail") 20 + 21 urepo := e.Get("repo").(*models.RepoActor) 22 23 var req ComAtprotoServerConfirmEmailRequest 24 if err := e.Bind(&req); err != nil { 25 + logger.Error("error binding", "error", err) 26 return helpers.ServerError(e, nil) 27 } 28 ··· 44 45 now := time.Now().UTC() 46 47 + if err := s.db.Exec(ctx, "UPDATE repos SET email_verification_code = NULL, email_verification_code_expires_at = NULL, email_confirmed_at = ? WHERE did = ?", nil, now, urepo.Repo.Did).Error; err != nil { 48 + logger.Error("error updating user", "error", err) 49 return helpers.ServerError(e, nil) 50 } 51
+76 -41
server/handle_server_create_account.go
··· 25 Handle string `json:"handle" validate:"required,atproto-handle"` 26 Did *string `json:"did" validate:"atproto-did"` 27 Password string `json:"password" validate:"required"` 28 - InviteCode string `json:"inviteCode" validate:"required"` 29 } 30 31 type ComAtprotoServerCreateAccountResponse struct { ··· 36 } 37 38 func (s *Server) handleCreateAccount(e echo.Context) error { 39 var request ComAtprotoServerCreateAccountRequest 40 41 if err := e.Bind(&request); err != nil { 42 - s.logger.Error("error receiving request", "endpoint", "com.atproto.server.createAccount", "error", err) 43 return helpers.ServerError(e, nil) 44 } 45 46 request.Handle = strings.ToLower(request.Handle) 47 48 if err := e.Validate(request); err != nil { 49 - s.logger.Error("error validating request", "endpoint", "com.atproto.server.createAccount", "error", err) 50 51 var verr ValidationError 52 if errors.As(err, &verr) { ··· 68 } 69 } 70 } 71 - 72 var signupDid string 73 if request.Did != nil { 74 - signupDid = *request.Did; 75 - 76 token := strings.TrimSpace(strings.Replace(e.Request().Header.Get("authorization"), "Bearer ", "", 1)) 77 if token == "" { 78 return helpers.UnauthorizedError(e, to.StringPtr("must authenticate to use an existing did")) ··· 80 authDid, err := s.validateServiceAuth(e.Request().Context(), token, "com.atproto.server.createAccount") 81 82 if err != nil { 83 - s.logger.Warn("error validating authorization token", "endpoint", "com.atproto.server.createAccount", "error", err) 84 return helpers.UnauthorizedError(e, to.StringPtr("invalid authorization token")) 85 } 86 ··· 90 } 91 92 // see if the handle is already taken 93 - actor, err := s.getActorByHandle(request.Handle) 94 if err != nil && err != gorm.ErrRecordNotFound { 95 - s.logger.Error("error looking up handle in db", "endpoint", "com.atproto.server.createAccount", "error", err) 96 return helpers.ServerError(e, nil) 97 } 98 if err == nil && actor.Did != signupDid { ··· 104 } 105 106 var ic models.InviteCode 107 - if err := s.db.Raw("SELECT * FROM invite_codes WHERE code = ?", nil, request.InviteCode).Scan(&ic).Error; err != nil { 108 - if err == gorm.ErrRecordNotFound { 109 return helpers.InputError(e, to.StringPtr("InvalidInviteCode")) 110 } 111 - s.logger.Error("error getting invite code from db", "error", err) 112 - return helpers.ServerError(e, nil) 113 - } 114 115 - if ic.RemainingUseCount < 1 { 116 - return helpers.InputError(e, to.StringPtr("InvalidInviteCode")) 117 } 118 119 // see if the email is already taken 120 - existingRepo, err := s.getRepoByEmail(request.Email) 121 if err != nil && err != gorm.ErrRecordNotFound { 122 - s.logger.Error("error looking up email in db", "endpoint", "com.atproto.server.createAccount", "error", err) 123 return helpers.ServerError(e, nil) 124 } 125 if err == nil && existingRepo.Did != signupDid { ··· 128 129 // TODO: unsupported domains 130 131 - k, err := atcrypto.GeneratePrivateKeyK256() 132 - if err != nil { 133 - s.logger.Error("error creating signing key", "endpoint", "com.atproto.server.createAccount", "error", err) 134 - return helpers.ServerError(e, nil) 135 } 136 137 if signupDid == "" { 138 did, op, err := s.plcClient.CreateDID(k, "", request.Handle) 139 if err != nil { 140 - s.logger.Error("error creating operation", "endpoint", "com.atproto.server.createAccount", "error", err) 141 return helpers.ServerError(e, nil) 142 } 143 144 if err := s.plcClient.SendOperation(e.Request().Context(), did, op); err != nil { 145 - s.logger.Error("error sending plc op", "endpoint", "com.atproto.server.createAccount", "error", err) 146 return helpers.ServerError(e, nil) 147 } 148 signupDid = did ··· 150 151 hashed, err := bcrypt.GenerateFromPassword([]byte(request.Password), 10) 152 if err != nil { 153 - s.logger.Error("error hashing password", "error", err) 154 return helpers.ServerError(e, nil) 155 } 156 ··· 169 Handle: request.Handle, 170 } 171 172 - if err := s.db.Create(&urepo, nil).Error; err != nil { 173 - s.logger.Error("error inserting new repo", "error", err) 174 return helpers.ServerError(e, nil) 175 } 176 - 177 - if err := s.db.Create(&actor, nil).Error; err != nil { 178 - s.logger.Error("error inserting new actor", "error", err) 179 return helpers.ServerError(e, nil) 180 } 181 } else { 182 - if err := s.db.Save(&actor, nil).Error; err != nil { 183 - s.logger.Error("error inserting new actor", "error", err) 184 return helpers.ServerError(e, nil) 185 } 186 } ··· 191 192 root, rev, err := r.Commit(context.TODO(), urepo.SignFor) 193 if err != nil { 194 - s.logger.Error("error committing", "error", err) 195 return helpers.ServerError(e, nil) 196 } 197 198 if err := s.UpdateRepo(context.TODO(), urepo.Did, root, rev); err != nil { 199 - s.logger.Error("error updating repo after commit", "error", err) 200 return helpers.ServerError(e, nil) 201 } 202 ··· 210 }) 211 } 212 213 - if err := s.db.Raw("UPDATE invite_codes SET remaining_use_count = remaining_use_count - 1 WHERE code = ?", nil, request.InviteCode).Scan(&ic).Error; err != nil { 214 - s.logger.Error("error decrementing use count", "error", err) 215 - return helpers.ServerError(e, nil) 216 } 217 218 - sess, err := s.createSession(&urepo) 219 if err != nil { 220 - s.logger.Error("error creating new session", "error", err) 221 return helpers.ServerError(e, nil) 222 } 223 224 go func() { 225 if err := s.sendEmailVerification(urepo.Email, actor.Handle, *urepo.EmailVerificationCode); err != nil { 226 - s.logger.Error("error sending email verification email", "error", err) 227 } 228 if err := s.sendWelcomeMail(urepo.Email, actor.Handle); err != nil { 229 - s.logger.Error("error sending welcome email", "error", err) 230 } 231 }() 232
··· 25 Handle string `json:"handle" validate:"required,atproto-handle"` 26 Did *string `json:"did" validate:"atproto-did"` 27 Password string `json:"password" validate:"required"` 28 + InviteCode string `json:"inviteCode" validate:"omitempty"` 29 } 30 31 type ComAtprotoServerCreateAccountResponse struct { ··· 36 } 37 38 func (s *Server) handleCreateAccount(e echo.Context) error { 39 + ctx := e.Request().Context() 40 + logger := s.logger.With("name", "handleServerCreateAccount") 41 + 42 var request ComAtprotoServerCreateAccountRequest 43 44 if err := e.Bind(&request); err != nil { 45 + logger.Error("error receiving request", "endpoint", "com.atproto.server.createAccount", "error", err) 46 return helpers.ServerError(e, nil) 47 } 48 49 request.Handle = strings.ToLower(request.Handle) 50 51 if err := e.Validate(request); err != nil { 52 + logger.Error("error validating request", "endpoint", "com.atproto.server.createAccount", "error", err) 53 54 var verr ValidationError 55 if errors.As(err, &verr) { ··· 71 } 72 } 73 } 74 + 75 var signupDid string 76 if request.Did != nil { 77 + signupDid = *request.Did 78 + 79 token := strings.TrimSpace(strings.Replace(e.Request().Header.Get("authorization"), "Bearer ", "", 1)) 80 if token == "" { 81 return helpers.UnauthorizedError(e, to.StringPtr("must authenticate to use an existing did")) ··· 83 authDid, err := s.validateServiceAuth(e.Request().Context(), token, "com.atproto.server.createAccount") 84 85 if err != nil { 86 + logger.Warn("error validating authorization token", "endpoint", "com.atproto.server.createAccount", "error", err) 87 return helpers.UnauthorizedError(e, to.StringPtr("invalid authorization token")) 88 } 89 ··· 93 } 94 95 // see if the handle is already taken 96 + actor, err := s.getActorByHandle(ctx, request.Handle) 97 if err != nil && err != gorm.ErrRecordNotFound { 98 + logger.Error("error looking up handle in db", "endpoint", "com.atproto.server.createAccount", "error", err) 99 return helpers.ServerError(e, nil) 100 } 101 if err == nil && actor.Did != signupDid { ··· 107 } 108 109 var ic models.InviteCode 110 + if s.config.RequireInvite { 111 + if strings.TrimSpace(request.InviteCode) == "" { 112 return helpers.InputError(e, to.StringPtr("InvalidInviteCode")) 113 } 114 + 115 + if err := s.db.Raw(ctx, "SELECT * FROM invite_codes WHERE code = ?", nil, request.InviteCode).Scan(&ic).Error; err != nil { 116 + if err == gorm.ErrRecordNotFound { 117 + return helpers.InputError(e, to.StringPtr("InvalidInviteCode")) 118 + } 119 + logger.Error("error getting invite code from db", "error", err) 120 + return helpers.ServerError(e, nil) 121 + } 122 123 + if ic.RemainingUseCount < 1 { 124 + return helpers.InputError(e, to.StringPtr("InvalidInviteCode")) 125 + } 126 } 127 128 // see if the email is already taken 129 + existingRepo, err := s.getRepoByEmail(ctx, request.Email) 130 if err != nil && err != gorm.ErrRecordNotFound { 131 + logger.Error("error looking up email in db", "endpoint", "com.atproto.server.createAccount", "error", err) 132 return helpers.ServerError(e, nil) 133 } 134 if err == nil && existingRepo.Did != signupDid { ··· 137 138 // TODO: unsupported domains 139 140 + var k *atcrypto.PrivateKeyK256 141 + 142 + if signupDid != "" { 143 + reservedKey, err := s.getReservedKey(ctx, signupDid) 144 + if err != nil { 145 + logger.Error("error looking up reserved key", "error", err) 146 + } 147 + if reservedKey != nil { 148 + k, err = atcrypto.ParsePrivateBytesK256(reservedKey.PrivateKey) 149 + if err != nil { 150 + logger.Error("error parsing reserved key", "error", err) 151 + k = nil 152 + } else { 153 + defer func() { 154 + if delErr := s.deleteReservedKey(ctx, reservedKey.KeyDid, reservedKey.Did); delErr != nil { 155 + logger.Error("error deleting reserved key", "error", delErr) 156 + } 157 + }() 158 + } 159 + } 160 + } 161 + 162 + if k == nil { 163 + k, err = atcrypto.GeneratePrivateKeyK256() 164 + if err != nil { 165 + logger.Error("error creating signing key", "endpoint", "com.atproto.server.createAccount", "error", err) 166 + return helpers.ServerError(e, nil) 167 + } 168 } 169 170 if signupDid == "" { 171 did, op, err := s.plcClient.CreateDID(k, "", request.Handle) 172 if err != nil { 173 + logger.Error("error creating operation", "endpoint", "com.atproto.server.createAccount", "error", err) 174 return helpers.ServerError(e, nil) 175 } 176 177 if err := s.plcClient.SendOperation(e.Request().Context(), did, op); err != nil { 178 + logger.Error("error sending plc op", "endpoint", "com.atproto.server.createAccount", "error", err) 179 return helpers.ServerError(e, nil) 180 } 181 signupDid = did ··· 183 184 hashed, err := bcrypt.GenerateFromPassword([]byte(request.Password), 10) 185 if err != nil { 186 + logger.Error("error hashing password", "error", err) 187 return helpers.ServerError(e, nil) 188 } 189 ··· 202 Handle: request.Handle, 203 } 204 205 + if err := s.db.Create(ctx, &urepo, nil).Error; err != nil { 206 + logger.Error("error inserting new repo", "error", err) 207 return helpers.ServerError(e, nil) 208 } 209 + 210 + if err := s.db.Create(ctx, &actor, nil).Error; err != nil { 211 + logger.Error("error inserting new actor", "error", err) 212 return helpers.ServerError(e, nil) 213 } 214 } else { 215 + if err := s.db.Save(ctx, &actor, nil).Error; err != nil { 216 + logger.Error("error inserting new actor", "error", err) 217 return helpers.ServerError(e, nil) 218 } 219 } ··· 224 225 root, rev, err := r.Commit(context.TODO(), urepo.SignFor) 226 if err != nil { 227 + logger.Error("error committing", "error", err) 228 return helpers.ServerError(e, nil) 229 } 230 231 if err := s.UpdateRepo(context.TODO(), urepo.Did, root, rev); err != nil { 232 + logger.Error("error updating repo after commit", "error", err) 233 return helpers.ServerError(e, nil) 234 } 235 ··· 243 }) 244 } 245 246 + if s.config.RequireInvite { 247 + if err := s.db.Raw(ctx, "UPDATE invite_codes SET remaining_use_count = remaining_use_count - 1 WHERE code = ?", nil, request.InviteCode).Scan(&ic).Error; err != nil { 248 + logger.Error("error decrementing use count", "error", err) 249 + return helpers.ServerError(e, nil) 250 + } 251 } 252 253 + sess, err := s.createSession(ctx, &urepo) 254 if err != nil { 255 + logger.Error("error creating new session", "error", err) 256 return helpers.ServerError(e, nil) 257 } 258 259 go func() { 260 if err := s.sendEmailVerification(urepo.Email, actor.Handle, *urepo.EmailVerificationCode); err != nil { 261 + logger.Error("error sending email verification email", "error", err) 262 } 263 if err := s.sendWelcomeMail(urepo.Email, actor.Handle); err != nil { 264 + logger.Error("error sending welcome email", "error", err) 265 } 266 }() 267
+7 -4
server/handle_server_create_invite_code.go
··· 17 } 18 19 func (s *Server) handleCreateInviteCode(e echo.Context) error { 20 var req ComAtprotoServerCreateInviteCodeRequest 21 if err := e.Bind(&req); err != nil { 22 - s.logger.Error("error binding", "error", err) 23 return helpers.ServerError(e, nil) 24 } 25 26 if err := e.Validate(req); err != nil { 27 - s.logger.Error("error validating", "error", err) 28 return helpers.InputError(e, nil) 29 } 30 ··· 37 acc = *req.ForAccount 38 } 39 40 - if err := s.db.Create(&models.InviteCode{ 41 Code: ic, 42 Did: acc, 43 RemainingUseCount: req.UseCount, 44 }, nil).Error; err != nil { 45 - s.logger.Error("error creating invite code", "error", err) 46 return helpers.ServerError(e, nil) 47 } 48
··· 17 } 18 19 func (s *Server) handleCreateInviteCode(e echo.Context) error { 20 + ctx := e.Request().Context() 21 + logger := s.logger.With("name", "handleServerCreateInviteCode") 22 + 23 var req ComAtprotoServerCreateInviteCodeRequest 24 if err := e.Bind(&req); err != nil { 25 + logger.Error("error binding", "error", err) 26 return helpers.ServerError(e, nil) 27 } 28 29 if err := e.Validate(req); err != nil { 30 + logger.Error("error validating", "error", err) 31 return helpers.InputError(e, nil) 32 } 33 ··· 40 acc = *req.ForAccount 41 } 42 43 + if err := s.db.Create(ctx, &models.InviteCode{ 44 Code: ic, 45 Did: acc, 46 RemainingUseCount: req.UseCount, 47 }, nil).Error; err != nil { 48 + logger.Error("error creating invite code", "error", err) 49 return helpers.ServerError(e, nil) 50 } 51
+7 -4
server/handle_server_create_invite_codes.go
··· 22 } 23 24 func (s *Server) handleCreateInviteCodes(e echo.Context) error { 25 var req ComAtprotoServerCreateInviteCodesRequest 26 if err := e.Bind(&req); err != nil { 27 - s.logger.Error("error binding", "error", err) 28 return helpers.ServerError(e, nil) 29 } 30 31 if err := e.Validate(req); err != nil { 32 - s.logger.Error("error validating", "error", err) 33 return helpers.InputError(e, nil) 34 } 35 ··· 50 ic := uuid.NewString() 51 ics = append(ics, ic) 52 53 - if err := s.db.Create(&models.InviteCode{ 54 Code: ic, 55 Did: did, 56 RemainingUseCount: req.UseCount, 57 }, nil).Error; err != nil { 58 - s.logger.Error("error creating invite code", "error", err) 59 return helpers.ServerError(e, nil) 60 } 61 }
··· 22 } 23 24 func (s *Server) handleCreateInviteCodes(e echo.Context) error { 25 + ctx := e.Request().Context() 26 + logger := s.logger.With("name", "handleServerCreateInviteCodes") 27 + 28 var req ComAtprotoServerCreateInviteCodesRequest 29 if err := e.Bind(&req); err != nil { 30 + logger.Error("error binding", "error", err) 31 return helpers.ServerError(e, nil) 32 } 33 34 if err := e.Validate(req); err != nil { 35 + logger.Error("error validating", "error", err) 36 return helpers.InputError(e, nil) 37 } 38 ··· 53 ic := uuid.NewString() 54 ics = append(ics, ic) 55 56 + if err := s.db.Create(ctx, &models.InviteCode{ 57 Code: ic, 58 Did: did, 59 RemainingUseCount: req.UseCount, 60 }, nil).Error; err != nil { 61 + logger.Error("error creating invite code", "error", err) 62 return helpers.ServerError(e, nil) 63 } 64 }
+65 -9
server/handle_server_create_session.go
··· 1 package server 2 3 import ( 4 "errors" 5 "strings" 6 7 "github.com/Azure/go-autorest/autorest/to" 8 "github.com/bluesky-social/indigo/atproto/syntax" ··· 32 } 33 34 func (s *Server) handleCreateSession(e echo.Context) error { 35 var req ComAtprotoServerCreateSessionRequest 36 if err := e.Bind(&req); err != nil { 37 - s.logger.Error("error binding request", "endpoint", "com.atproto.server.serverCreateSession", "error", err) 38 return helpers.ServerError(e, nil) 39 } 40 ··· 65 var err error 66 switch idtype { 67 case "did": 68 - err = s.db.Raw("SELECT r.*, a.* FROM repos r LEFT JOIN actors a ON r.did = a.did WHERE r.did = ?", nil, req.Identifier).Scan(&repo).Error 69 case "handle": 70 - err = s.db.Raw("SELECT r.*, a.* FROM actors a LEFT JOIN repos r ON a.did = r.did WHERE a.handle = ?", nil, req.Identifier).Scan(&repo).Error 71 case "email": 72 - err = s.db.Raw("SELECT r.*, a.* FROM repos r LEFT JOIN actors a ON r.did = a.did WHERE r.email = ?", nil, req.Identifier).Scan(&repo).Error 73 } 74 75 if err != nil { ··· 77 return helpers.InputError(e, to.StringPtr("InvalidRequest")) 78 } 79 80 - s.logger.Error("erorr looking up repo", "endpoint", "com.atproto.server.createSession", "error", err) 81 return helpers.ServerError(e, nil) 82 } 83 84 if err := bcrypt.CompareHashAndPassword([]byte(repo.Password), []byte(req.Password)); err != nil { 85 if err != bcrypt.ErrMismatchedHashAndPassword { 86 - s.logger.Error("erorr comparing hash and password", "error", err) 87 } 88 return helpers.InputError(e, to.StringPtr("InvalidRequest")) 89 } 90 91 - sess, err := s.createSession(&repo.Repo) 92 if err != nil { 93 - s.logger.Error("error creating session", "error", err) 94 return helpers.ServerError(e, nil) 95 } 96 ··· 101 Did: repo.Repo.Did, 102 Email: repo.Email, 103 EmailConfirmed: repo.EmailConfirmedAt != nil, 104 - EmailAuthFactor: false, 105 Active: repo.Active(), 106 Status: repo.Status(), 107 }) 108 }
··· 1 package server 2 3 import ( 4 + "context" 5 "errors" 6 + "fmt" 7 "strings" 8 + "time" 9 10 "github.com/Azure/go-autorest/autorest/to" 11 "github.com/bluesky-social/indigo/atproto/syntax" ··· 35 } 36 37 func (s *Server) handleCreateSession(e echo.Context) error { 38 + ctx := e.Request().Context() 39 + logger := s.logger.With("name", "handleServerCreateSession") 40 + 41 var req ComAtprotoServerCreateSessionRequest 42 if err := e.Bind(&req); err != nil { 43 + logger.Error("error binding request", "endpoint", "com.atproto.server.serverCreateSession", "error", err) 44 return helpers.ServerError(e, nil) 45 } 46 ··· 71 var err error 72 switch idtype { 73 case "did": 74 + err = s.db.Raw(ctx, "SELECT r.*, a.* FROM repos r LEFT JOIN actors a ON r.did = a.did WHERE r.did = ?", nil, req.Identifier).Scan(&repo).Error 75 case "handle": 76 + err = s.db.Raw(ctx, "SELECT r.*, a.* FROM actors a LEFT JOIN repos r ON a.did = r.did WHERE a.handle = ?", nil, req.Identifier).Scan(&repo).Error 77 case "email": 78 + err = s.db.Raw(ctx, "SELECT r.*, a.* FROM repos r LEFT JOIN actors a ON r.did = a.did WHERE r.email = ?", nil, req.Identifier).Scan(&repo).Error 79 } 80 81 if err != nil { ··· 83 return helpers.InputError(e, to.StringPtr("InvalidRequest")) 84 } 85 86 + logger.Error("erorr looking up repo", "endpoint", "com.atproto.server.createSession", "error", err) 87 return helpers.ServerError(e, nil) 88 } 89 90 if err := bcrypt.CompareHashAndPassword([]byte(repo.Password), []byte(req.Password)); err != nil { 91 if err != bcrypt.ErrMismatchedHashAndPassword { 92 + logger.Error("erorr comparing hash and password", "error", err) 93 } 94 return helpers.InputError(e, to.StringPtr("InvalidRequest")) 95 } 96 97 + // if repo requires 2FA token and one hasn't been provided, return error prompting for one 98 + if repo.TwoFactorType != models.TwoFactorTypeNone && (req.AuthFactorToken == nil || *req.AuthFactorToken == "") { 99 + err = s.createAndSendTwoFactorCode(ctx, repo) 100 + if err != nil { 101 + logger.Error("sending 2FA code", "error", err) 102 + return helpers.ServerError(e, nil) 103 + } 104 + 105 + return helpers.InputError(e, to.StringPtr("AuthFactorTokenRequired")) 106 + } 107 + 108 + // if 2FA is required, now check that the one provided is valid 109 + if repo.TwoFactorType != models.TwoFactorTypeNone { 110 + if repo.TwoFactorCode == nil || repo.TwoFactorCodeExpiresAt == nil { 111 + err = s.createAndSendTwoFactorCode(ctx, repo) 112 + if err != nil { 113 + logger.Error("sending 2FA code", "error", err) 114 + return helpers.ServerError(e, nil) 115 + } 116 + 117 + return helpers.InputError(e, to.StringPtr("AuthFactorTokenRequired")) 118 + } 119 + 120 + if *repo.TwoFactorCode != *req.AuthFactorToken { 121 + return helpers.InvalidTokenError(e) 122 + } 123 + 124 + if time.Now().UTC().After(*repo.TwoFactorCodeExpiresAt) { 125 + return helpers.ExpiredTokenError(e) 126 + } 127 + } 128 + 129 + sess, err := s.createSession(ctx, &repo.Repo) 130 if err != nil { 131 + logger.Error("error creating session", "error", err) 132 return helpers.ServerError(e, nil) 133 } 134 ··· 139 Did: repo.Repo.Did, 140 Email: repo.Email, 141 EmailConfirmed: repo.EmailConfirmedAt != nil, 142 + EmailAuthFactor: repo.TwoFactorType != models.TwoFactorTypeNone, 143 Active: repo.Active(), 144 Status: repo.Status(), 145 }) 146 } 147 + 148 + func (s *Server) createAndSendTwoFactorCode(ctx context.Context, repo models.RepoActor) error { 149 + // TODO: when implementing a new type of 2FA there should be some logic in here to send the 150 + // right type of code 151 + 152 + code := fmt.Sprintf("%s-%s", helpers.RandomVarchar(5), helpers.RandomVarchar(5)) 153 + eat := time.Now().Add(10 * time.Minute).UTC() 154 + 155 + if err := s.db.Exec(ctx, "UPDATE repos SET two_factor_code = ?, two_factor_code_expires_at = ? WHERE did = ?", nil, code, eat, repo.Repo.Did).Error; err != nil { 156 + return fmt.Errorf("updating repo: %w", err) 157 + } 158 + 159 + if err := s.sendTwoFactorCode(repo.Email, repo.Handle, code); err != nil { 160 + return fmt.Errorf("sending email: %w", err) 161 + } 162 + 163 + return nil 164 + }
+6 -3
server/handle_server_deactivate_account.go
··· 19 } 20 21 func (s *Server) handleServerDeactivateAccount(e echo.Context) error { 22 var req ComAtprotoServerDeactivateAccountRequest 23 if err := e.Bind(&req); err != nil { 24 - s.logger.Error("error binding", "error", err) 25 return helpers.ServerError(e, nil) 26 } 27 28 urepo := e.Get("repo").(*models.RepoActor) 29 30 - if err := s.db.Exec("UPDATE repos SET deactivated = ? WHERE did = ?", nil, true, urepo.Repo.Did).Error; err != nil { 31 - s.logger.Error("error updating account status to deactivated", "error", err) 32 return helpers.ServerError(e, nil) 33 } 34
··· 19 } 20 21 func (s *Server) handleServerDeactivateAccount(e echo.Context) error { 22 + ctx := e.Request().Context() 23 + logger := s.logger.With("name", "handleServerDeactivateAccount") 24 + 25 var req ComAtprotoServerDeactivateAccountRequest 26 if err := e.Bind(&req); err != nil { 27 + logger.Error("error binding", "error", err) 28 return helpers.ServerError(e, nil) 29 } 30 31 urepo := e.Get("repo").(*models.RepoActor) 32 33 + if err := s.db.Exec(ctx, "UPDATE repos SET deactivated = ? WHERE did = ?", nil, true, urepo.Repo.Did).Error; err != nil { 34 + logger.Error("error updating account status to deactivated", "error", err) 35 return helpers.ServerError(e, nil) 36 } 37
+150
server/handle_server_delete_account.go
···
··· 1 + package server 2 + 3 + import ( 4 + "context" 5 + "time" 6 + 7 + "github.com/Azure/go-autorest/autorest/to" 8 + "github.com/bluesky-social/indigo/api/atproto" 9 + "github.com/bluesky-social/indigo/events" 10 + "github.com/bluesky-social/indigo/util" 11 + "github.com/haileyok/cocoon/internal/helpers" 12 + "github.com/labstack/echo/v4" 13 + "golang.org/x/crypto/bcrypt" 14 + ) 15 + 16 + type ComAtprotoServerDeleteAccountRequest struct { 17 + Did string `json:"did" validate:"required"` 18 + Password string `json:"password" validate:"required"` 19 + Token string `json:"token" validate:"required"` 20 + } 21 + 22 + func (s *Server) handleServerDeleteAccount(e echo.Context) error { 23 + ctx := e.Request().Context() 24 + logger := s.logger.With("name", "handleServerDeleteAccount") 25 + 26 + var req ComAtprotoServerDeleteAccountRequest 27 + if err := e.Bind(&req); err != nil { 28 + logger.Error("error binding", "error", err) 29 + return helpers.ServerError(e, nil) 30 + } 31 + 32 + if err := e.Validate(&req); err != nil { 33 + logger.Error("error validating", "error", err) 34 + return helpers.ServerError(e, nil) 35 + } 36 + 37 + urepo, err := s.getRepoActorByDid(ctx, req.Did) 38 + if err != nil { 39 + logger.Error("error getting repo", "error", err) 40 + return echo.NewHTTPError(400, "account not found") 41 + } 42 + 43 + if err := bcrypt.CompareHashAndPassword([]byte(urepo.Repo.Password), []byte(req.Password)); err != nil { 44 + logger.Error("password mismatch", "error", err) 45 + return echo.NewHTTPError(401, "Invalid did or password") 46 + } 47 + 48 + if urepo.Repo.AccountDeleteCode == nil || urepo.Repo.AccountDeleteCodeExpiresAt == nil { 49 + logger.Error("no deletion token found for account") 50 + return echo.NewHTTPError(400, map[string]interface{}{ 51 + "error": "InvalidToken", 52 + "message": "Token is invalid", 53 + }) 54 + } 55 + 56 + if *urepo.Repo.AccountDeleteCode != req.Token { 57 + logger.Error("deletion token mismatch") 58 + return echo.NewHTTPError(400, map[string]interface{}{ 59 + "error": "InvalidToken", 60 + "message": "Token is invalid", 61 + }) 62 + } 63 + 64 + if time.Now().UTC().After(*urepo.Repo.AccountDeleteCodeExpiresAt) { 65 + logger.Error("deletion token expired") 66 + return echo.NewHTTPError(400, map[string]interface{}{ 67 + "error": "ExpiredToken", 68 + "message": "Token is expired", 69 + }) 70 + } 71 + 72 + tx := s.db.BeginDangerously(ctx) 73 + if tx.Error != nil { 74 + logger.Error("error starting transaction", "error", tx.Error) 75 + return helpers.ServerError(e, nil) 76 + } 77 + 78 + status := "error" 79 + func() { 80 + if status == "error" { 81 + if err := tx.Rollback().Error; err != nil { 82 + logger.Error("error rolling back after delete failure", "err", err) 83 + } 84 + } 85 + }() 86 + 87 + if err := tx.Exec("DELETE FROM blocks WHERE did = ?", nil, req.Did).Error; err != nil { 88 + logger.Error("error deleting blocks", "error", err) 89 + return helpers.ServerError(e, nil) 90 + } 91 + 92 + if err := tx.Exec("DELETE FROM records WHERE did = ?", nil, req.Did).Error; err != nil { 93 + logger.Error("error deleting records", "error", err) 94 + return helpers.ServerError(e, nil) 95 + } 96 + 97 + if err := tx.Exec("DELETE FROM blobs WHERE did = ?", nil, req.Did).Error; err != nil { 98 + logger.Error("error deleting blobs", "error", err) 99 + return helpers.ServerError(e, nil) 100 + } 101 + 102 + if err := tx.Exec("DELETE FROM tokens WHERE did = ?", nil, req.Did).Error; err != nil { 103 + logger.Error("error deleting tokens", "error", err) 104 + return helpers.ServerError(e, nil) 105 + } 106 + 107 + if err := tx.Exec("DELETE FROM refresh_tokens WHERE did = ?", nil, req.Did).Error; err != nil { 108 + logger.Error("error deleting refresh tokens", "error", err) 109 + return helpers.ServerError(e, nil) 110 + } 111 + 112 + if err := tx.Exec("DELETE FROM reserved_keys WHERE did = ?", nil, req.Did).Error; err != nil { 113 + logger.Error("error deleting reserved keys", "error", err) 114 + return helpers.ServerError(e, nil) 115 + } 116 + 117 + if err := tx.Exec("DELETE FROM invite_codes WHERE did = ?", nil, req.Did).Error; err != nil { 118 + logger.Error("error deleting invite codes", "error", err) 119 + return helpers.ServerError(e, nil) 120 + } 121 + 122 + if err := tx.Exec("DELETE FROM actors WHERE did = ?", nil, req.Did).Error; err != nil { 123 + logger.Error("error deleting actor", "error", err) 124 + return helpers.ServerError(e, nil) 125 + } 126 + 127 + if err := tx.Exec("DELETE FROM repos WHERE did = ?", nil, req.Did).Error; err != nil { 128 + logger.Error("error deleting repo", "error", err) 129 + return helpers.ServerError(e, nil) 130 + } 131 + 132 + status = "ok" 133 + 134 + if err := tx.Commit().Error; err != nil { 135 + logger.Error("error committing transaction", "error", err) 136 + return helpers.ServerError(e, nil) 137 + } 138 + 139 + s.evtman.AddEvent(context.TODO(), &events.XRPCStreamEvent{ 140 + RepoAccount: &atproto.SyncSubscribeRepos_Account{ 141 + Active: false, 142 + Did: req.Did, 143 + Status: to.StringPtr("deleted"), 144 + Seq: time.Now().UnixMicro(), 145 + Time: time.Now().Format(util.ISO8601), 146 + }, 147 + }) 148 + 149 + return e.NoContent(200) 150 + }
+4 -2
server/handle_server_delete_session.go
··· 7 ) 8 9 func (s *Server) handleDeleteSession(e echo.Context) error { 10 token := e.Get("token").(string) 11 12 var acctok models.Token 13 - if err := s.db.Raw("DELETE FROM tokens WHERE token = ? RETURNING *", nil, token).Scan(&acctok).Error; err != nil { 14 s.logger.Error("error deleting access token from db", "error", err) 15 return helpers.ServerError(e, nil) 16 } 17 18 - if err := s.db.Exec("DELETE FROM refresh_tokens WHERE token = ?", nil, acctok.RefreshToken).Error; err != nil { 19 s.logger.Error("error deleting refresh token from db", "error", err) 20 return helpers.ServerError(e, nil) 21 }
··· 7 ) 8 9 func (s *Server) handleDeleteSession(e echo.Context) error { 10 + ctx := e.Request().Context() 11 + 12 token := e.Get("token").(string) 13 14 var acctok models.Token 15 + if err := s.db.Raw(ctx, "DELETE FROM tokens WHERE token = ? RETURNING *", nil, token).Scan(&acctok).Error; err != nil { 16 s.logger.Error("error deleting access token from db", "error", err) 17 return helpers.ServerError(e, nil) 18 } 19 20 + if err := s.db.Exec(ctx, "DELETE FROM refresh_tokens WHERE token = ?", nil, acctok.RefreshToken).Error; err != nil { 21 s.logger.Error("error deleting refresh token from db", "error", err) 22 return helpers.ServerError(e, nil) 23 }
+1 -1
server/handle_server_describe_server.go
··· 22 23 func (s *Server) handleDescribeServer(e echo.Context) error { 24 return e.JSON(200, ComAtprotoServerDescribeServerResponse{ 25 - InviteCodeRequired: true, 26 PhoneVerificationRequired: false, 27 AvailableUserDomains: []string{"." + s.config.Hostname}, // TODO: more 28 Links: ComAtprotoServerDescribeServerResponseLinks{
··· 22 23 func (s *Server) handleDescribeServer(e echo.Context) error { 24 return e.JSON(200, ComAtprotoServerDescribeServerResponse{ 25 + InviteCodeRequired: s.config.RequireInvite, 26 PhoneVerificationRequired: false, 27 AvailableUserDomains: []string{"." + s.config.Hostname}, // TODO: more 28 Links: ComAtprotoServerDescribeServerResponseLinks{
+17 -8
server/handle_server_get_service_auth.go
··· 21 Aud string `query:"aud" validate:"required,atproto-did"` 22 // exp should be a float, as some clients will send a non-integer expiration 23 Exp float64 `query:"exp"` 24 - Lxm string `query:"lxm" validate:"required,atproto-nsid"` 25 } 26 27 func (s *Server) handleServerGetServiceAuth(e echo.Context) error { 28 var req ServerGetServiceAuthRequest 29 if err := e.Bind(&req); err != nil { 30 - s.logger.Error("could not bind service auth request", "error", err) 31 return helpers.ServerError(e, nil) 32 } 33 ··· 45 return helpers.InputError(e, to.StringPtr("may not generate auth tokens recursively")) 46 } 47 48 - maxExp := now + (60 * 30) 49 if exp > maxExp { 50 return helpers.InputError(e, to.StringPtr("expiration too big. smoller please")) 51 } ··· 59 } 60 hj, err := json.Marshal(header) 61 if err != nil { 62 - s.logger.Error("error marshaling header", "error", err) 63 return helpers.ServerError(e, nil) 64 } 65 ··· 68 payload := map[string]any{ 69 "iss": repo.Repo.Did, 70 "aud": req.Aud, 71 - "lxm": req.Lxm, 72 "jti": uuid.NewString(), 73 "exp": exp, 74 "iat": now, 75 } 76 pj, err := json.Marshal(payload) 77 if err != nil { 78 - s.logger.Error("error marashaling payload", "error", err) 79 return helpers.ServerError(e, nil) 80 } 81 ··· 86 87 sk, err := secp256k1secec.NewPrivateKey(repo.SigningKey) 88 if err != nil { 89 - s.logger.Error("can't load private key", "error", err) 90 return err 91 } 92 93 R, S, _, err := sk.SignRaw(rand.Reader, hash[:]) 94 if err != nil { 95 - s.logger.Error("error signing", "error", err) 96 return helpers.ServerError(e, nil) 97 } 98
··· 21 Aud string `query:"aud" validate:"required,atproto-did"` 22 // exp should be a float, as some clients will send a non-integer expiration 23 Exp float64 `query:"exp"` 24 + Lxm string `query:"lxm"` 25 } 26 27 func (s *Server) handleServerGetServiceAuth(e echo.Context) error { 28 + logger := s.logger.With("name", "handleServerGetServiceAuth") 29 + 30 var req ServerGetServiceAuthRequest 31 if err := e.Bind(&req); err != nil { 32 + logger.Error("could not bind service auth request", "error", err) 33 return helpers.ServerError(e, nil) 34 } 35 ··· 47 return helpers.InputError(e, to.StringPtr("may not generate auth tokens recursively")) 48 } 49 50 + var maxExp int64 51 + if req.Lxm != "" { 52 + maxExp = now + (60 * 60) 53 + } else { 54 + maxExp = now + 60 55 + } 56 if exp > maxExp { 57 return helpers.InputError(e, to.StringPtr("expiration too big. smoller please")) 58 } ··· 66 } 67 hj, err := json.Marshal(header) 68 if err != nil { 69 + logger.Error("error marshaling header", "error", err) 70 return helpers.ServerError(e, nil) 71 } 72 ··· 75 payload := map[string]any{ 76 "iss": repo.Repo.Did, 77 "aud": req.Aud, 78 "jti": uuid.NewString(), 79 "exp": exp, 80 "iat": now, 81 + } 82 + if req.Lxm != "" { 83 + payload["lxm"] = req.Lxm 84 } 85 pj, err := json.Marshal(payload) 86 if err != nil { 87 + logger.Error("error marashaling payload", "error", err) 88 return helpers.ServerError(e, nil) 89 } 90 ··· 95 96 sk, err := secp256k1secec.NewPrivateKey(repo.SigningKey) 97 if err != nil { 98 + logger.Error("can't load private key", "error", err) 99 return err 100 } 101 102 R, S, _, err := sk.SignRaw(rand.Reader, hash[:]) 103 if err != nil { 104 + logger.Error("error signing", "error", err) 105 return helpers.ServerError(e, nil) 106 } 107
+1 -1
server/handle_server_get_session.go
··· 23 Did: repo.Repo.Did, 24 Email: repo.Email, 25 EmailConfirmed: repo.EmailConfirmedAt != nil, 26 - EmailAuthFactor: false, // TODO: todo todo 27 Active: repo.Active(), 28 Status: repo.Status(), 29 })
··· 23 Did: repo.Repo.Did, 24 Email: repo.Email, 25 EmailConfirmed: repo.EmailConfirmedAt != nil, 26 + EmailAuthFactor: repo.TwoFactorType != models.TwoFactorTypeNone, 27 Active: repo.Active(), 28 Status: repo.Status(), 29 })
+9 -6
server/handle_server_refresh_session.go
··· 16 } 17 18 func (s *Server) handleRefreshSession(e echo.Context) error { 19 token := e.Get("token").(string) 20 repo := e.Get("repo").(*models.RepoActor) 21 22 - if err := s.db.Exec("DELETE FROM refresh_tokens WHERE token = ?", nil, token).Error; err != nil { 23 - s.logger.Error("error getting refresh token from db", "error", err) 24 return helpers.ServerError(e, nil) 25 } 26 27 - if err := s.db.Exec("DELETE FROM tokens WHERE refresh_token = ?", nil, token).Error; err != nil { 28 - s.logger.Error("error deleting access token from db", "error", err) 29 return helpers.ServerError(e, nil) 30 } 31 32 - sess, err := s.createSession(&repo.Repo) 33 if err != nil { 34 - s.logger.Error("error creating new session for refresh", "error", err) 35 return helpers.ServerError(e, nil) 36 } 37
··· 16 } 17 18 func (s *Server) handleRefreshSession(e echo.Context) error { 19 + ctx := e.Request().Context() 20 + logger := s.logger.With("name", "handleServerRefreshSession") 21 + 22 token := e.Get("token").(string) 23 repo := e.Get("repo").(*models.RepoActor) 24 25 + if err := s.db.Exec(ctx, "DELETE FROM refresh_tokens WHERE token = ?", nil, token).Error; err != nil { 26 + logger.Error("error getting refresh token from db", "error", err) 27 return helpers.ServerError(e, nil) 28 } 29 30 + if err := s.db.Exec(ctx, "DELETE FROM tokens WHERE refresh_token = ?", nil, token).Error; err != nil { 31 + logger.Error("error deleting access token from db", "error", err) 32 return helpers.ServerError(e, nil) 33 } 34 35 + sess, err := s.createSession(ctx, &repo.Repo) 36 if err != nil { 37 + logger.Error("error creating new session for refresh", "error", err) 38 return helpers.ServerError(e, nil) 39 } 40
+52
server/handle_server_request_account_delete.go
···
··· 1 + package server 2 + 3 + import ( 4 + "fmt" 5 + "time" 6 + 7 + "github.com/haileyok/cocoon/internal/helpers" 8 + "github.com/haileyok/cocoon/models" 9 + "github.com/labstack/echo/v4" 10 + ) 11 + 12 + func (s *Server) handleServerRequestAccountDelete(e echo.Context) error { 13 + ctx := e.Request().Context() 14 + logger := s.logger.With("name", "handleServerRequestAccountDelete") 15 + 16 + urepo := e.Get("repo").(*models.RepoActor) 17 + 18 + token := fmt.Sprintf("%s-%s", helpers.RandomVarchar(5), helpers.RandomVarchar(5)) 19 + expiresAt := time.Now().UTC().Add(15 * time.Minute) 20 + 21 + if err := s.db.Exec(ctx, "UPDATE repos SET account_delete_code = ?, account_delete_code_expires_at = ? WHERE did = ?", nil, token, expiresAt, urepo.Repo.Did).Error; err != nil { 22 + logger.Error("error setting deletion token", "error", err) 23 + return helpers.ServerError(e, nil) 24 + } 25 + 26 + if urepo.Email != "" { 27 + if err := s.sendAccountDeleteEmail(urepo.Email, urepo.Actor.Handle, token); err != nil { 28 + logger.Error("error sending account deletion email", "error", err) 29 + } 30 + } 31 + 32 + return e.NoContent(200) 33 + } 34 + 35 + func (s *Server) sendAccountDeleteEmail(email, handle, token string) error { 36 + if s.mail == nil { 37 + return nil 38 + } 39 + 40 + s.mailLk.Lock() 41 + defer s.mailLk.Unlock() 42 + 43 + s.mail.To(email) 44 + s.mail.Subject("Account Deletion Request for " + s.config.Hostname) 45 + 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)) 46 + 47 + if err := s.mail.Send(); err != nil { 48 + return err 49 + } 50 + 51 + return nil 52 + }
+6 -3
server/handle_server_request_email_confirmation.go
··· 11 ) 12 13 func (s *Server) handleServerRequestEmailConfirmation(e echo.Context) error { 14 urepo := e.Get("repo").(*models.RepoActor) 15 16 if urepo.EmailConfirmedAt != nil { ··· 20 code := fmt.Sprintf("%s-%s", helpers.RandomVarchar(5), helpers.RandomVarchar(5)) 21 eat := time.Now().Add(10 * time.Minute).UTC() 22 23 - if err := s.db.Exec("UPDATE repos SET email_verification_code = ?, email_verification_code_expires_at = ? WHERE did = ?", nil, code, eat, urepo.Repo.Did).Error; err != nil { 24 - s.logger.Error("error updating user", "error", err) 25 return helpers.ServerError(e, nil) 26 } 27 28 if err := s.sendEmailVerification(urepo.Email, urepo.Handle, code); err != nil { 29 - s.logger.Error("error sending mail", "error", err) 30 return helpers.ServerError(e, nil) 31 } 32
··· 11 ) 12 13 func (s *Server) handleServerRequestEmailConfirmation(e echo.Context) error { 14 + ctx := e.Request().Context() 15 + logger := s.logger.With("name", "handleServerRequestEmailConfirm") 16 + 17 urepo := e.Get("repo").(*models.RepoActor) 18 19 if urepo.EmailConfirmedAt != nil { ··· 23 code := fmt.Sprintf("%s-%s", helpers.RandomVarchar(5), helpers.RandomVarchar(5)) 24 eat := time.Now().Add(10 * time.Minute).UTC() 25 26 + if err := s.db.Exec(ctx, "UPDATE repos SET email_verification_code = ?, email_verification_code_expires_at = ? WHERE did = ?", nil, code, eat, urepo.Repo.Did).Error; err != nil { 27 + logger.Error("error updating user", "error", err) 28 return helpers.ServerError(e, nil) 29 } 30 31 if err := s.sendEmailVerification(urepo.Email, urepo.Handle, code); err != nil { 32 + logger.Error("error sending mail", "error", err) 33 return helpers.ServerError(e, nil) 34 } 35
+6 -3
server/handle_server_request_email_update.go
··· 14 } 15 16 func (s *Server) handleServerRequestEmailUpdate(e echo.Context) error { 17 urepo := e.Get("repo").(*models.RepoActor) 18 19 if urepo.EmailConfirmedAt != nil { 20 code := fmt.Sprintf("%s-%s", helpers.RandomVarchar(5), helpers.RandomVarchar(5)) 21 eat := time.Now().Add(10 * time.Minute).UTC() 22 23 - if err := s.db.Exec("UPDATE repos SET email_update_code = ?, email_update_code_expires_at = ? WHERE did = ?", nil, code, eat, urepo.Repo.Did).Error; err != nil { 24 - s.logger.Error("error updating repo", "error", err) 25 return helpers.ServerError(e, nil) 26 } 27 28 if err := s.sendEmailUpdate(urepo.Email, urepo.Handle, code); err != nil { 29 - s.logger.Error("error sending email", "error", err) 30 return helpers.ServerError(e, nil) 31 } 32 }
··· 14 } 15 16 func (s *Server) handleServerRequestEmailUpdate(e echo.Context) error { 17 + ctx := e.Request().Context() 18 + logger := s.logger.With("name", "handleServerRequestEmailUpdate") 19 + 20 urepo := e.Get("repo").(*models.RepoActor) 21 22 if urepo.EmailConfirmedAt != nil { 23 code := fmt.Sprintf("%s-%s", helpers.RandomVarchar(5), helpers.RandomVarchar(5)) 24 eat := time.Now().Add(10 * time.Minute).UTC() 25 26 + if err := s.db.Exec(ctx, "UPDATE repos SET email_update_code = ?, email_update_code_expires_at = ? WHERE did = ?", nil, code, eat, urepo.Repo.Did).Error; err != nil { 27 + logger.Error("error updating repo", "error", err) 28 return helpers.ServerError(e, nil) 29 } 30 31 if err := s.sendEmailUpdate(urepo.Email, urepo.Handle, code); err != nil { 32 + logger.Error("error sending email", "error", err) 33 return helpers.ServerError(e, nil) 34 } 35 }
+7 -4
server/handle_server_request_password_reset.go
··· 14 } 15 16 func (s *Server) handleServerRequestPasswordReset(e echo.Context) error { 17 urepo, ok := e.Get("repo").(*models.RepoActor) 18 if !ok { 19 var req ComAtprotoServerRequestPasswordResetRequest ··· 25 return err 26 } 27 28 - murepo, err := s.getRepoActorByEmail(req.Email) 29 if err != nil { 30 return err 31 } ··· 36 code := fmt.Sprintf("%s-%s", helpers.RandomVarchar(5), helpers.RandomVarchar(5)) 37 eat := time.Now().Add(10 * time.Minute).UTC() 38 39 - if err := s.db.Exec("UPDATE repos SET password_reset_code = ?, password_reset_code_expires_at = ? WHERE did = ?", nil, code, eat, urepo.Repo.Did).Error; err != nil { 40 - s.logger.Error("error updating repo", "error", err) 41 return helpers.ServerError(e, nil) 42 } 43 44 if err := s.sendPasswordReset(urepo.Email, urepo.Handle, code); err != nil { 45 - s.logger.Error("error sending email", "error", err) 46 return helpers.ServerError(e, nil) 47 } 48
··· 14 } 15 16 func (s *Server) handleServerRequestPasswordReset(e echo.Context) error { 17 + ctx := e.Request().Context() 18 + logger := s.logger.With("name", "handleServerRequestPasswordReset") 19 + 20 urepo, ok := e.Get("repo").(*models.RepoActor) 21 if !ok { 22 var req ComAtprotoServerRequestPasswordResetRequest ··· 28 return err 29 } 30 31 + murepo, err := s.getRepoActorByEmail(ctx, req.Email) 32 if err != nil { 33 return err 34 } ··· 39 code := fmt.Sprintf("%s-%s", helpers.RandomVarchar(5), helpers.RandomVarchar(5)) 40 eat := time.Now().Add(10 * time.Minute).UTC() 41 42 + if err := s.db.Exec(ctx, "UPDATE repos SET password_reset_code = ?, password_reset_code_expires_at = ? WHERE did = ?", nil, code, eat, urepo.Repo.Did).Error; err != nil { 43 + logger.Error("error updating repo", "error", err) 44 return helpers.ServerError(e, nil) 45 } 46 47 if err := s.sendPasswordReset(urepo.Email, urepo.Handle, code); err != nil { 48 + logger.Error("error sending email", "error", err) 49 return helpers.ServerError(e, nil) 50 } 51
+99
server/handle_server_reserve_signing_key.go
···
··· 1 + package server 2 + 3 + import ( 4 + "context" 5 + "time" 6 + 7 + "github.com/bluesky-social/indigo/atproto/atcrypto" 8 + "github.com/haileyok/cocoon/internal/helpers" 9 + "github.com/haileyok/cocoon/models" 10 + "github.com/labstack/echo/v4" 11 + ) 12 + 13 + type ServerReserveSigningKeyRequest struct { 14 + Did *string `json:"did"` 15 + } 16 + 17 + type ServerReserveSigningKeyResponse struct { 18 + SigningKey string `json:"signingKey"` 19 + } 20 + 21 + func (s *Server) handleServerReserveSigningKey(e echo.Context) error { 22 + ctx := e.Request().Context() 23 + logger := s.logger.With("name", "handleServerReserveSigningKey") 24 + 25 + var req ServerReserveSigningKeyRequest 26 + if err := e.Bind(&req); err != nil { 27 + logger.Error("could not bind reserve signing key request", "error", err) 28 + return helpers.ServerError(e, nil) 29 + } 30 + 31 + if req.Did != nil && *req.Did != "" { 32 + var existing models.ReservedKey 33 + if err := s.db.Raw(ctx, "SELECT * FROM reserved_keys WHERE did = ?", nil, *req.Did).Scan(&existing).Error; err == nil && existing.KeyDid != "" { 34 + return e.JSON(200, ServerReserveSigningKeyResponse{ 35 + SigningKey: existing.KeyDid, 36 + }) 37 + } 38 + } 39 + 40 + k, err := atcrypto.GeneratePrivateKeyK256() 41 + if err != nil { 42 + logger.Error("error creating signing key", "endpoint", "com.atproto.server.reserveSigningKey", "error", err) 43 + return helpers.ServerError(e, nil) 44 + } 45 + 46 + pubKey, err := k.PublicKey() 47 + if err != nil { 48 + logger.Error("error getting public key", "endpoint", "com.atproto.server.reserveSigningKey", "error", err) 49 + return helpers.ServerError(e, nil) 50 + } 51 + 52 + keyDid := pubKey.DIDKey() 53 + 54 + reservedKey := models.ReservedKey{ 55 + KeyDid: keyDid, 56 + Did: req.Did, 57 + PrivateKey: k.Bytes(), 58 + CreatedAt: time.Now(), 59 + } 60 + 61 + if err := s.db.Create(ctx, &reservedKey, nil).Error; err != nil { 62 + logger.Error("error storing reserved key", "endpoint", "com.atproto.server.reserveSigningKey", "error", err) 63 + return helpers.ServerError(e, nil) 64 + } 65 + 66 + logger.Info("reserved signing key", "keyDid", keyDid, "forDid", req.Did) 67 + 68 + return e.JSON(200, ServerReserveSigningKeyResponse{ 69 + SigningKey: keyDid, 70 + }) 71 + } 72 + 73 + func (s *Server) getReservedKey(ctx context.Context, keyDidOrDid string) (*models.ReservedKey, error) { 74 + var reservedKey models.ReservedKey 75 + 76 + if err := s.db.Raw(ctx, "SELECT * FROM reserved_keys WHERE key_did = ?", nil, keyDidOrDid).Scan(&reservedKey).Error; err == nil && reservedKey.KeyDid != "" { 77 + return &reservedKey, nil 78 + } 79 + 80 + if err := s.db.Raw(ctx, "SELECT * FROM reserved_keys WHERE did = ?", nil, keyDidOrDid).Scan(&reservedKey).Error; err == nil && reservedKey.KeyDid != "" { 81 + return &reservedKey, nil 82 + } 83 + 84 + return nil, nil 85 + } 86 + 87 + func (s *Server) deleteReservedKey(ctx context.Context, keyDid string, did *string) error { 88 + if err := s.db.Exec(ctx, "DELETE FROM reserved_keys WHERE key_did = ?", nil, keyDid).Error; err != nil { 89 + return err 90 + } 91 + 92 + if did != nil && *did != "" { 93 + if err := s.db.Exec(ctx, "DELETE FROM reserved_keys WHERE did = ?", nil, *did).Error; err != nil { 94 + return err 95 + } 96 + } 97 + 98 + return nil 99 + }
+7 -4
server/handle_server_reset_password.go
··· 16 } 17 18 func (s *Server) handleServerResetPassword(e echo.Context) error { 19 urepo := e.Get("repo").(*models.RepoActor) 20 21 var req ComAtprotoServerResetPasswordRequest 22 if err := e.Bind(&req); err != nil { 23 - s.logger.Error("error binding", "error", err) 24 return helpers.ServerError(e, nil) 25 } 26 ··· 42 43 hash, err := bcrypt.GenerateFromPassword([]byte(req.Password), 10) 44 if err != nil { 45 - s.logger.Error("error creating hash", "error", err) 46 return helpers.ServerError(e, nil) 47 } 48 49 - if err := s.db.Exec("UPDATE repos SET password_reset_code = NULL, password_reset_code_expires_at = NULL, password = ? WHERE did = ?", nil, hash, urepo.Repo.Did).Error; err != nil { 50 - s.logger.Error("error updating repo", "error", err) 51 return helpers.ServerError(e, nil) 52 } 53
··· 16 } 17 18 func (s *Server) handleServerResetPassword(e echo.Context) error { 19 + ctx := e.Request().Context() 20 + logger := s.logger.With("name", "handleServerResetPassword") 21 + 22 urepo := e.Get("repo").(*models.RepoActor) 23 24 var req ComAtprotoServerResetPasswordRequest 25 if err := e.Bind(&req); err != nil { 26 + logger.Error("error binding", "error", err) 27 return helpers.ServerError(e, nil) 28 } 29 ··· 45 46 hash, err := bcrypt.GenerateFromPassword([]byte(req.Password), 10) 47 if err != nil { 48 + logger.Error("error creating hash", "error", err) 49 return helpers.ServerError(e, nil) 50 } 51 52 + if err := s.db.Exec(ctx, "UPDATE repos SET password_reset_code = NULL, password_reset_code_expires_at = NULL, password = ? WHERE did = ?", nil, hash, urepo.Repo.Did).Error; err != nil { 53 + logger.Error("error updating repo", "error", err) 54 return helpers.ServerError(e, nil) 55 } 56
+3 -1
server/handle_server_resolve_handle.go
··· 10 ) 11 12 func (s *Server) handleResolveHandle(e echo.Context) error { 13 type Resp struct { 14 Did string `json:"did"` 15 } ··· 28 ctx := context.WithValue(e.Request().Context(), "skip-cache", true) 29 did, err := s.passport.ResolveHandle(ctx, parsed.String()) 30 if err != nil { 31 - s.logger.Error("error resolving handle", "error", err) 32 return helpers.ServerError(e, nil) 33 } 34
··· 10 ) 11 12 func (s *Server) handleResolveHandle(e echo.Context) error { 13 + logger := s.logger.With("name", "handleServerResolveHandle") 14 + 15 type Resp struct { 16 Did string `json:"did"` 17 } ··· 30 ctx := context.WithValue(e.Request().Context(), "skip-cache", true) 31 did, err := s.passport.ResolveHandle(ctx, parsed.String()) 32 if err != nil { 33 + logger.Error("error resolving handle", "error", err) 34 return helpers.ServerError(e, nil) 35 } 36
+34 -9
server/handle_server_update_email.go
··· 11 type ComAtprotoServerUpdateEmailRequest struct { 12 Email string `json:"email" validate:"required"` 13 EmailAuthFactor bool `json:"emailAuthFactor"` 14 - Token string `json:"token" validate:"required"` 15 } 16 17 func (s *Server) handleServerUpdateEmail(e echo.Context) error { 18 urepo := e.Get("repo").(*models.RepoActor) 19 20 var req ComAtprotoServerUpdateEmailRequest 21 if err := e.Bind(&req); err != nil { 22 - s.logger.Error("error binding", "error", err) 23 return helpers.ServerError(e, nil) 24 } 25 ··· 27 return helpers.InputError(e, nil) 28 } 29 30 - if urepo.EmailUpdateCode == nil || urepo.EmailUpdateCodeExpiresAt == nil { 31 return helpers.InvalidTokenError(e) 32 } 33 34 - if *urepo.EmailUpdateCode != req.Token { 35 - return helpers.InvalidTokenError(e) 36 } 37 38 - if time.Now().UTC().After(*urepo.EmailUpdateCodeExpiresAt) { 39 - return helpers.ExpiredTokenError(e) 40 } 41 42 - if err := s.db.Exec("UPDATE repos SET email_update_code = NULL, email_update_code_expires_at = NULL, email_confirmed_at = NULL, email = ? WHERE did = ?", nil, req.Email, urepo.Repo.Did).Error; err != nil { 43 - s.logger.Error("error updating repo", "error", err) 44 return helpers.ServerError(e, nil) 45 } 46
··· 11 type ComAtprotoServerUpdateEmailRequest struct { 12 Email string `json:"email" validate:"required"` 13 EmailAuthFactor bool `json:"emailAuthFactor"` 14 + Token string `json:"token"` 15 } 16 17 func (s *Server) handleServerUpdateEmail(e echo.Context) error { 18 + ctx := e.Request().Context() 19 + logger := s.logger.With("name", "handleServerUpdateEmail") 20 + 21 urepo := e.Get("repo").(*models.RepoActor) 22 23 var req ComAtprotoServerUpdateEmailRequest 24 if err := e.Bind(&req); err != nil { 25 + logger.Error("error binding", "error", err) 26 return helpers.ServerError(e, nil) 27 } 28 ··· 30 return helpers.InputError(e, nil) 31 } 32 33 + // To disable email auth factor a token is required. 34 + // To enable email auth factor a token is not required. 35 + // If updating an email address, a token will be sent anyway 36 + if urepo.TwoFactorType != models.TwoFactorTypeNone && req.EmailAuthFactor == false && req.Token == "" { 37 return helpers.InvalidTokenError(e) 38 } 39 40 + if req.Token != "" { 41 + if urepo.EmailUpdateCode == nil || urepo.EmailUpdateCodeExpiresAt == nil { 42 + return helpers.InvalidTokenError(e) 43 + } 44 + 45 + if *urepo.EmailUpdateCode != req.Token { 46 + return helpers.InvalidTokenError(e) 47 + } 48 + 49 + if time.Now().UTC().After(*urepo.EmailUpdateCodeExpiresAt) { 50 + return helpers.ExpiredTokenError(e) 51 + } 52 + } 53 + 54 + twoFactorType := models.TwoFactorTypeNone 55 + if req.EmailAuthFactor { 56 + twoFactorType = models.TwoFactorTypeEmail 57 } 58 59 + query := "UPDATE repos SET email_update_code = NULL, email_update_code_expires_at = NULL, two_factor_type = ?, email = ?" 60 + 61 + if urepo.Email != req.Email { 62 + query += ",email_confirmed_at = NULL" 63 } 64 65 + query += " WHERE did = ?" 66 + 67 + if err := s.db.Exec(ctx, query, nil, twoFactorType, req.Email, urepo.Repo.Did).Error; err != nil { 68 + logger.Error("error updating repo", "error", err) 69 return helpers.ServerError(e, nil) 70 } 71
+23 -13
server/handle_sync_get_blob.go
··· 17 ) 18 19 func (s *Server) handleSyncGetBlob(e echo.Context) error { 20 did := e.QueryParam("did") 21 if did == "" { 22 return helpers.InputError(e, nil) ··· 32 return helpers.InputError(e, nil) 33 } 34 35 - urepo, err := s.getRepoActorByDid(did) 36 if err != nil { 37 - s.logger.Error("could not find user for requested blob", "error", err) 38 return helpers.InputError(e, nil) 39 } 40 ··· 46 } 47 48 var blob models.Blob 49 - if err := s.db.Raw("SELECT * FROM blobs WHERE did = ? AND cid = ?", nil, did, c.Bytes()).Scan(&blob).Error; err != nil { 50 - s.logger.Error("error looking up blob", "error", err) 51 return helpers.ServerError(e, nil) 52 } 53 ··· 55 56 if blob.Storage == "sqlite" { 57 var parts []models.BlobPart 58 - if err := s.db.Raw("SELECT * FROM blob_parts WHERE blob_id = ? ORDER BY idx", nil, blob.ID).Scan(&parts).Error; err != nil { 59 - s.logger.Error("error getting blob parts", "error", err) 60 return helpers.ServerError(e, nil) 61 } 62 ··· 65 buf.Write(p.Data) 66 } 67 } else if blob.Storage == "s3" { 68 - if !(s.s3Config != nil && s.s3Config.BlobstoreEnabled) { 69 - s.logger.Error("s3 storage disabled") 70 return helpers.ServerError(e, nil) 71 } 72 73 config := &aws.Config{ ··· 82 83 sess, err := session.NewSession(config) 84 if err != nil { 85 - s.logger.Error("error creating aws session", "error", err) 86 return helpers.ServerError(e, nil) 87 } 88 89 svc := s3.New(sess) 90 if result, err := svc.GetObject(&s3.GetObjectInput{ 91 Bucket: aws.String(s.s3Config.Bucket), 92 - Key: aws.String(fmt.Sprintf("blobs/%s/%s", urepo.Repo.Did, c.String())), 93 }); err != nil { 94 - s.logger.Error("error getting blob from s3", "error", err) 95 return helpers.ServerError(e, nil) 96 } else { 97 read := 0 ··· 105 break 106 } 107 } else if err != nil && err != io.ErrUnexpectedEOF { 108 - s.logger.Error("error reading blob", "error", err) 109 return helpers.ServerError(e, nil) 110 } 111 ··· 116 } 117 } 118 } else { 119 - s.logger.Error("unknown storage", "storage", blob.Storage) 120 return helpers.ServerError(e, nil) 121 } 122
··· 17 ) 18 19 func (s *Server) handleSyncGetBlob(e echo.Context) error { 20 + ctx := e.Request().Context() 21 + logger := s.logger.With("name", "handleSyncGetBlob") 22 + 23 did := e.QueryParam("did") 24 if did == "" { 25 return helpers.InputError(e, nil) ··· 35 return helpers.InputError(e, nil) 36 } 37 38 + urepo, err := s.getRepoActorByDid(ctx, did) 39 if err != nil { 40 + logger.Error("could not find user for requested blob", "error", err) 41 return helpers.InputError(e, nil) 42 } 43 ··· 49 } 50 51 var blob models.Blob 52 + if err := s.db.Raw(ctx, "SELECT * FROM blobs WHERE did = ? AND cid = ?", nil, did, c.Bytes()).Scan(&blob).Error; err != nil { 53 + logger.Error("error looking up blob", "error", err) 54 return helpers.ServerError(e, nil) 55 } 56 ··· 58 59 if blob.Storage == "sqlite" { 60 var parts []models.BlobPart 61 + if err := s.db.Raw(ctx, "SELECT * FROM blob_parts WHERE blob_id = ? ORDER BY idx", nil, blob.ID).Scan(&parts).Error; err != nil { 62 + logger.Error("error getting blob parts", "error", err) 63 return helpers.ServerError(e, nil) 64 } 65 ··· 68 buf.Write(p.Data) 69 } 70 } else if blob.Storage == "s3" { 71 + if !(s.s3Config != nil && s.s3Config.BlobstoreEnabled) { 72 + logger.Error("s3 storage disabled") 73 return helpers.ServerError(e, nil) 74 + } 75 + 76 + blobKey := fmt.Sprintf("blobs/%s/%s", urepo.Repo.Did, c.String()) 77 + 78 + if s.s3Config.CDNUrl != "" { 79 + redirectUrl := fmt.Sprintf("%s/%s", s.s3Config.CDNUrl, blobKey) 80 + return e.Redirect(302, redirectUrl) 81 } 82 83 config := &aws.Config{ ··· 92 93 sess, err := session.NewSession(config) 94 if err != nil { 95 + logger.Error("error creating aws session", "error", err) 96 return helpers.ServerError(e, nil) 97 } 98 99 svc := s3.New(sess) 100 if result, err := svc.GetObject(&s3.GetObjectInput{ 101 Bucket: aws.String(s.s3Config.Bucket), 102 + Key: aws.String(blobKey), 103 }); err != nil { 104 + logger.Error("error getting blob from s3", "error", err) 105 return helpers.ServerError(e, nil) 106 } else { 107 read := 0 ··· 115 break 116 } 117 } else if err != nil && err != io.ErrUnexpectedEOF { 118 + logger.Error("error reading blob", "error", err) 119 return helpers.ServerError(e, nil) 120 } 121 ··· 126 } 127 } 128 } else { 129 + logger.Error("unknown storage", "storage", blob.Storage) 130 return helpers.ServerError(e, nil) 131 } 132
+3 -2
server/handle_sync_get_blocks.go
··· 18 19 func (s *Server) handleGetBlocks(e echo.Context) error { 20 ctx := e.Request().Context() 21 22 var req ComAtprotoSyncGetBlocksRequest 23 if err := e.Bind(&req); err != nil { ··· 35 cids = append(cids, c) 36 } 37 38 - urepo, err := s.getRepoActorByDid(req.Did) 39 if err != nil { 40 return helpers.ServerError(e, nil) 41 } ··· 52 }) 53 54 if _, err := carstore.LdWrite(buf, hb); err != nil { 55 - s.logger.Error("error writing to car", "error", err) 56 return helpers.ServerError(e, nil) 57 } 58
··· 18 19 func (s *Server) handleGetBlocks(e echo.Context) error { 20 ctx := e.Request().Context() 21 + logger := s.logger.With("name", "handleSyncGetBlocks") 22 23 var req ComAtprotoSyncGetBlocksRequest 24 if err := e.Bind(&req); err != nil { ··· 36 cids = append(cids, c) 37 } 38 39 + urepo, err := s.getRepoActorByDid(ctx, req.Did) 40 if err != nil { 41 return helpers.ServerError(e, nil) 42 } ··· 53 }) 54 55 if _, err := carstore.LdWrite(buf, hb); err != nil { 56 + logger.Error("error writing to car", "error", err) 57 return helpers.ServerError(e, nil) 58 } 59
+3 -1
server/handle_sync_get_latest_commit.go
··· 12 } 13 14 func (s *Server) handleSyncGetLatestCommit(e echo.Context) error { 15 did := e.QueryParam("did") 16 if did == "" { 17 return helpers.InputError(e, nil) 18 } 19 20 - urepo, err := s.getRepoActorByDid(did) 21 if err != nil { 22 return err 23 }
··· 12 } 13 14 func (s *Server) handleSyncGetLatestCommit(e echo.Context) error { 15 + ctx := e.Request().Context() 16 + 17 did := e.QueryParam("did") 18 if did == "" { 19 return helpers.InputError(e, nil) 20 } 21 22 + urepo, err := s.getRepoActorByDid(ctx, did) 23 if err != nil { 24 return err 25 }
+8 -5
server/handle_sync_get_record.go
··· 13 ) 14 15 func (s *Server) handleSyncGetRecord(e echo.Context) error { 16 did := e.QueryParam("did") 17 collection := e.QueryParam("collection") 18 rkey := e.QueryParam("rkey") 19 20 var urepo models.Repo 21 - if err := s.db.Raw("SELECT * FROM repos WHERE did = ?", nil, did).Scan(&urepo).Error; err != nil { 22 - s.logger.Error("error getting repo", "error", err) 23 return helpers.ServerError(e, nil) 24 } 25 26 - root, blocks, err := s.repoman.getRecordProof(urepo, collection, rkey) 27 if err != nil { 28 return err 29 } ··· 36 }) 37 38 if _, err := carstore.LdWrite(buf, hb); err != nil { 39 - s.logger.Error("error writing to car", "error", err) 40 return helpers.ServerError(e, nil) 41 } 42 43 for _, blk := range blocks { 44 if _, err := carstore.LdWrite(buf, blk.Cid().Bytes(), blk.RawData()); err != nil { 45 - s.logger.Error("error writing to car", "error", err) 46 return helpers.ServerError(e, nil) 47 } 48 }
··· 13 ) 14 15 func (s *Server) handleSyncGetRecord(e echo.Context) error { 16 + ctx := e.Request().Context() 17 + logger := s.logger.With("name", "handleSyncGetRecord") 18 + 19 did := e.QueryParam("did") 20 collection := e.QueryParam("collection") 21 rkey := e.QueryParam("rkey") 22 23 var urepo models.Repo 24 + if err := s.db.Raw(ctx, "SELECT * FROM repos WHERE did = ?", nil, did).Scan(&urepo).Error; err != nil { 25 + logger.Error("error getting repo", "error", err) 26 return helpers.ServerError(e, nil) 27 } 28 29 + root, blocks, err := s.repoman.getRecordProof(ctx, urepo, collection, rkey) 30 if err != nil { 31 return err 32 } ··· 39 }) 40 41 if _, err := carstore.LdWrite(buf, hb); err != nil { 42 + logger.Error("error writing to car", "error", err) 43 return helpers.ServerError(e, nil) 44 } 45 46 for _, blk := range blocks { 47 if _, err := carstore.LdWrite(buf, blk.Cid().Bytes(), blk.RawData()); err != nil { 48 + logger.Error("error writing to car", "error", err) 49 return helpers.ServerError(e, nil) 50 } 51 }
+6 -3
server/handle_sync_get_repo.go
··· 13 ) 14 15 func (s *Server) handleSyncGetRepo(e echo.Context) error { 16 did := e.QueryParam("did") 17 if did == "" { 18 return helpers.InputError(e, nil) 19 } 20 21 - urepo, err := s.getRepoActorByDid(did) 22 if err != nil { 23 return err 24 } ··· 36 buf := new(bytes.Buffer) 37 38 if _, err := carstore.LdWrite(buf, hb); err != nil { 39 - s.logger.Error("error writing to car", "error", err) 40 return helpers.ServerError(e, nil) 41 } 42 43 var blocks []models.Block 44 - if err := s.db.Raw("SELECT * FROM blocks WHERE did = ? ORDER BY rev ASC", nil, urepo.Repo.Did).Scan(&blocks).Error; err != nil { 45 return err 46 } 47
··· 13 ) 14 15 func (s *Server) handleSyncGetRepo(e echo.Context) error { 16 + ctx := e.Request().Context() 17 + logger := s.logger.With("name", "handleSyncGetRepo") 18 + 19 did := e.QueryParam("did") 20 if did == "" { 21 return helpers.InputError(e, nil) 22 } 23 24 + urepo, err := s.getRepoActorByDid(ctx, did) 25 if err != nil { 26 return err 27 } ··· 39 buf := new(bytes.Buffer) 40 41 if _, err := carstore.LdWrite(buf, hb); err != nil { 42 + logger.Error("error writing to car", "error", err) 43 return helpers.ServerError(e, nil) 44 } 45 46 var blocks []models.Block 47 + if err := s.db.Raw(ctx, "SELECT * FROM blocks WHERE did = ? ORDER BY rev ASC", nil, urepo.Repo.Did).Scan(&blocks).Error; err != nil { 48 return err 49 } 50
+3 -1
server/handle_sync_get_repo_status.go
··· 14 15 // TODO: make this actually do the right thing 16 func (s *Server) handleSyncGetRepoStatus(e echo.Context) error { 17 did := e.QueryParam("did") 18 if did == "" { 19 return helpers.InputError(e, nil) 20 } 21 22 - urepo, err := s.getRepoActorByDid(did) 23 if err != nil { 24 return err 25 }
··· 14 15 // TODO: make this actually do the right thing 16 func (s *Server) handleSyncGetRepoStatus(e echo.Context) error { 17 + ctx := e.Request().Context() 18 + 19 did := e.QueryParam("did") 20 if did == "" { 21 return helpers.InputError(e, nil) 22 } 23 24 + urepo, err := s.getRepoActorByDid(ctx, did) 25 if err != nil { 26 return err 27 }
+8 -5
server/handle_sync_list_blobs.go
··· 14 } 15 16 func (s *Server) handleSyncListBlobs(e echo.Context) error { 17 did := e.QueryParam("did") 18 if did == "" { 19 return helpers.InputError(e, nil) ··· 35 } 36 params = append(params, limit) 37 38 - urepo, err := s.getRepoActorByDid(did) 39 if err != nil { 40 - s.logger.Error("could not find user for requested blobs", "error", err) 41 return helpers.InputError(e, nil) 42 } 43 ··· 49 } 50 51 var blobs []models.Blob 52 - if err := s.db.Raw("SELECT * FROM blobs WHERE did = ? "+cursorquery+" ORDER BY created_at DESC LIMIT ?", nil, params...).Scan(&blobs).Error; err != nil { 53 - s.logger.Error("error getting records", "error", err) 54 return helpers.ServerError(e, nil) 55 } 56 ··· 58 for _, b := range blobs { 59 c, err := cid.Cast(b.Cid) 60 if err != nil { 61 - s.logger.Error("error casting cid", "error", err) 62 return helpers.ServerError(e, nil) 63 } 64 cstrs = append(cstrs, c.String())
··· 14 } 15 16 func (s *Server) handleSyncListBlobs(e echo.Context) error { 17 + ctx := e.Request().Context() 18 + logger := s.logger.With("name", "handleSyncListBlobs") 19 + 20 did := e.QueryParam("did") 21 if did == "" { 22 return helpers.InputError(e, nil) ··· 38 } 39 params = append(params, limit) 40 41 + urepo, err := s.getRepoActorByDid(ctx, did) 42 if err != nil { 43 + logger.Error("could not find user for requested blobs", "error", err) 44 return helpers.InputError(e, nil) 45 } 46 ··· 52 } 53 54 var blobs []models.Blob 55 + if err := s.db.Raw(ctx, "SELECT * FROM blobs WHERE did = ? "+cursorquery+" ORDER BY created_at DESC LIMIT ?", nil, params...).Scan(&blobs).Error; err != nil { 56 + logger.Error("error getting records", "error", err) 57 return helpers.ServerError(e, nil) 58 } 59 ··· 61 for _, b := range blobs { 62 c, err := cid.Cast(b.Cid) 63 if err != nil { 64 + logger.Error("error casting cid", "error", err) 65 return helpers.ServerError(e, nil) 66 } 67 cstrs = append(cstrs, c.String())
+82 -50
server/handle_sync_subscribe_repos.go
··· 7 "github.com/bluesky-social/indigo/events" 8 "github.com/bluesky-social/indigo/lex/util" 9 "github.com/btcsuite/websocket" 10 "github.com/labstack/echo/v4" 11 ) 12 13 func (s *Server) handleSyncSubscribeRepos(e echo.Context) error { 14 - ctx := e.Request().Context() 15 logger := s.logger.With("component", "subscribe-repos-websocket") 16 17 conn, err := websocket.Upgrade(e.Response().Writer, e.Request(), e.Response().Header(), 1<<10, 1<<10) ··· 24 logger = logger.With("ident", ident) 25 logger.Info("new connection established") 26 27 - evts, cancel, err := s.evtman.Subscribe(ctx, ident, func(evt *events.XRPCStreamEvent) bool { 28 return true 29 }, nil) 30 if err != nil { 31 return err 32 } 33 - defer cancel() 34 35 header := events.EventHeader{Op: events.EvtKindMessage} 36 for evt := range evts { 37 - wc, err := conn.NextWriter(websocket.BinaryMessage) 38 - if err != nil { 39 - logger.Error("error writing message to relay", "err", err) 40 - break 41 - } 42 43 - if ctx.Err() != nil { 44 - logger.Error("context error", "err", err) 45 - break 46 - } 47 48 - var obj util.CBOR 49 - switch { 50 - case evt.Error != nil: 51 - header.Op = events.EvtKindErrorFrame 52 - obj = evt.Error 53 - case evt.RepoCommit != nil: 54 - header.MsgType = "#commit" 55 - obj = evt.RepoCommit 56 - case evt.RepoIdentity != nil: 57 - header.MsgType = "#identity" 58 - obj = evt.RepoIdentity 59 - case evt.RepoAccount != nil: 60 - header.MsgType = "#account" 61 - obj = evt.RepoAccount 62 - case evt.RepoInfo != nil: 63 - header.MsgType = "#info" 64 - obj = evt.RepoInfo 65 - default: 66 - logger.Warn("unrecognized event kind") 67 - return nil 68 - } 69 70 - if err := header.MarshalCBOR(wc); err != nil { 71 - logger.Error("failed to write header to relay", "err", err) 72 - break 73 - } 74 75 - if err := obj.MarshalCBOR(wc); err != nil { 76 - logger.Error("failed to write event to relay", "err", err) 77 - break 78 - } 79 80 - if err := wc.Close(); err != nil { 81 - logger.Error("failed to flush-close our event write", "err", err) 82 - break 83 - } 84 } 85 86 // we should tell the relay to request a new crawl at this point if we got disconnected 87 // use a new context since the old one might be cancelled at this point 88 - ctx, cancel = context.WithTimeout(context.Background(), 10*time.Second) 89 - defer cancel() 90 - if err := s.requestCrawl(ctx); err != nil { 91 - logger.Error("error requesting crawls", "err", err) 92 - } 93 94 return nil 95 }
··· 7 "github.com/bluesky-social/indigo/events" 8 "github.com/bluesky-social/indigo/lex/util" 9 "github.com/btcsuite/websocket" 10 + "github.com/haileyok/cocoon/metrics" 11 "github.com/labstack/echo/v4" 12 ) 13 14 func (s *Server) handleSyncSubscribeRepos(e echo.Context) error { 15 + ctx, cancel := context.WithCancel(e.Request().Context()) 16 + defer cancel() 17 + 18 logger := s.logger.With("component", "subscribe-repos-websocket") 19 20 conn, err := websocket.Upgrade(e.Response().Writer, e.Request(), e.Response().Header(), 1<<10, 1<<10) ··· 27 logger = logger.With("ident", ident) 28 logger.Info("new connection established") 29 30 + metrics.RelaysConnected.WithLabelValues(ident).Inc() 31 + defer func() { 32 + metrics.RelaysConnected.WithLabelValues(ident).Dec() 33 + }() 34 + 35 + evts, evtManCancel, err := s.evtman.Subscribe(ctx, ident, func(evt *events.XRPCStreamEvent) bool { 36 return true 37 }, nil) 38 if err != nil { 39 return err 40 } 41 + defer evtManCancel() 42 + 43 + // drop the connection whenever a subscriber disconnects from the socket, we should get errors 44 + go func() { 45 + for { 46 + select { 47 + case <-ctx.Done(): 48 + return 49 + default: 50 + if _, _, err := conn.ReadMessage(); err != nil { 51 + logger.Warn("websocket error", "err", err) 52 + cancel() 53 + return 54 + } 55 + } 56 + } 57 + }() 58 59 header := events.EventHeader{Op: events.EvtKindMessage} 60 for evt := range evts { 61 + func() { 62 + defer func() { 63 + metrics.RelaySends.WithLabelValues(ident, header.MsgType).Inc() 64 + }() 65 66 + wc, err := conn.NextWriter(websocket.BinaryMessage) 67 + if err != nil { 68 + logger.Error("error writing message to relay", "err", err) 69 + return 70 + } 71 72 + if ctx.Err() != nil { 73 + logger.Error("context error", "err", err) 74 + return 75 + } 76 77 + var obj util.CBOR 78 + switch { 79 + case evt.Error != nil: 80 + header.Op = events.EvtKindErrorFrame 81 + obj = evt.Error 82 + case evt.RepoCommit != nil: 83 + header.MsgType = "#commit" 84 + obj = evt.RepoCommit 85 + case evt.RepoIdentity != nil: 86 + header.MsgType = "#identity" 87 + obj = evt.RepoIdentity 88 + case evt.RepoAccount != nil: 89 + header.MsgType = "#account" 90 + obj = evt.RepoAccount 91 + case evt.RepoInfo != nil: 92 + header.MsgType = "#info" 93 + obj = evt.RepoInfo 94 + default: 95 + logger.Warn("unrecognized event kind") 96 + return 97 + } 98 99 + if err := header.MarshalCBOR(wc); err != nil { 100 + logger.Error("failed to write header to relay", "err", err) 101 + return 102 + } 103 + 104 + if err := obj.MarshalCBOR(wc); err != nil { 105 + logger.Error("failed to write event to relay", "err", err) 106 + return 107 + } 108 109 + if err := wc.Close(); err != nil { 110 + logger.Error("failed to flush-close our event write", "err", err) 111 + return 112 + } 113 + }() 114 } 115 116 // we should tell the relay to request a new crawl at this point if we got disconnected 117 // use a new context since the old one might be cancelled at this point 118 + go func() { 119 + retryCtx, retryCancel := context.WithTimeout(context.Background(), 10*time.Second) 120 + defer retryCancel() 121 + if err := s.requestCrawl(retryCtx); err != nil { 122 + logger.Error("error requesting crawls", "err", err) 123 + } 124 + }() 125 126 return nil 127 }
+36
server/handle_well_known.go
··· 2 3 import ( 4 "fmt" 5 6 "github.com/Azure/go-autorest/autorest/to" 7 "github.com/labstack/echo/v4" 8 ) 9 10 var ( ··· 61 }, 62 }, 63 }) 64 } 65 66 func (s *Server) handleOauthProtectedResource(e echo.Context) error {
··· 2 3 import ( 4 "fmt" 5 + "strings" 6 7 "github.com/Azure/go-autorest/autorest/to" 8 + "github.com/haileyok/cocoon/internal/helpers" 9 "github.com/labstack/echo/v4" 10 + "gorm.io/gorm" 11 ) 12 13 var ( ··· 64 }, 65 }, 66 }) 67 + } 68 + 69 + func (s *Server) handleAtprotoDid(e echo.Context) error { 70 + ctx := e.Request().Context() 71 + logger := s.logger.With("name", "handleAtprotoDid") 72 + 73 + host := e.Request().Host 74 + if host == "" { 75 + return helpers.InputError(e, to.StringPtr("Invalid handle.")) 76 + } 77 + 78 + host = strings.Split(host, ":")[0] 79 + host = strings.ToLower(strings.TrimSpace(host)) 80 + 81 + if host == s.config.Hostname { 82 + return e.String(200, s.config.Did) 83 + } 84 + 85 + suffix := "." + s.config.Hostname 86 + if !strings.HasSuffix(host, suffix) { 87 + return e.NoContent(404) 88 + } 89 + 90 + actor, err := s.getActorByHandle(ctx, host) 91 + if err != nil { 92 + if err == gorm.ErrRecordNotFound { 93 + return e.NoContent(404) 94 + } 95 + logger.Error("error looking up actor by handle", "error", err) 96 + return helpers.ServerError(e, nil) 97 + } 98 + 99 + return e.String(200, actor.Did) 100 } 101 102 func (s *Server) handleOauthProtectedResource(e echo.Context) error {
+19
server/mail.go
··· 96 97 return nil 98 }
··· 96 97 return nil 98 } 99 + 100 + func (s *Server) sendTwoFactorCode(email, handle, code string) error { 101 + if s.mail == nil { 102 + return nil 103 + } 104 + 105 + s.mailLk.Lock() 106 + defer s.mailLk.Unlock() 107 + 108 + s.mail.To(email) 109 + s.mail.Subject("2FA code for " + s.config.Hostname) 110 + s.mail.Plain().Set(fmt.Sprintf("Hello %s. Your 2FA code is %s. This code will expire in ten minutes.", handle, code)) 111 + 112 + if err := s.mail.Send(); err != nil { 113 + return err 114 + } 115 + 116 + return nil 117 + }
+51 -23
server/middleware.go
··· 37 38 func (s *Server) handleLegacySessionMiddleware(next echo.HandlerFunc) echo.HandlerFunc { 39 return func(e echo.Context) error { 40 authheader := e.Request().Header.Get("authorization") 41 if authheader == "" { 42 return e.JSON(401, map[string]string{"error": "Unauthorized"}) ··· 67 if hasLxm { 68 pts := strings.Split(e.Request().URL.String(), "/") 69 if lxm != pts[len(pts)-1] { 70 - s.logger.Error("service auth lxm incorrect", "lxm", lxm, "expected", pts[len(pts)-1], "error", err) 71 return helpers.InputError(e, nil) 72 } 73 74 maybeDid, ok := claims["iss"].(string) 75 if !ok { 76 - s.logger.Error("no iss in service auth token", "error", err) 77 return helpers.InputError(e, nil) 78 } 79 did = maybeDid 80 81 - maybeRepo, err := s.getRepoActorByDid(did) 82 if err != nil { 83 - s.logger.Error("error fetching repo", "error", err) 84 return helpers.ServerError(e, nil) 85 } 86 repo = maybeRepo ··· 94 return s.privateKey.Public(), nil 95 }) 96 if err != nil { 97 - s.logger.Error("error parsing jwt", "error", err) 98 return helpers.ExpiredTokenError(e) 99 } 100 ··· 107 hash := sha256.Sum256([]byte(signingInput)) 108 sigBytes, err := base64.RawURLEncoding.DecodeString(kpts[2]) 109 if err != nil { 110 - s.logger.Error("error decoding signature bytes", "error", err) 111 return helpers.ServerError(e, nil) 112 } 113 114 if len(sigBytes) != 64 { 115 - s.logger.Error("incorrect sigbytes length", "length", len(sigBytes)) 116 return helpers.ServerError(e, nil) 117 } 118 ··· 121 rr, _ := secp256k1.NewScalarFromBytes((*[32]byte)(rBytes)) 122 ss, _ := secp256k1.NewScalarFromBytes((*[32]byte)(sBytes)) 123 124 sk, err := secp256k1secec.NewPrivateKey(repo.SigningKey) 125 if err != nil { 126 - s.logger.Error("can't load private key", "error", err) 127 return err 128 } 129 130 pubKey, ok := sk.Public().(*secp256k1secec.PublicKey) 131 if !ok { 132 - s.logger.Error("error getting public key from sk") 133 return helpers.ServerError(e, nil) 134 } 135 136 verified := pubKey.VerifyRaw(hash[:], rr, ss) 137 if !verified { 138 - s.logger.Error("error verifying", "error", err) 139 return helpers.ServerError(e, nil) 140 } 141 } ··· 159 Found bool 160 } 161 var result Result 162 - if err := s.db.Raw("SELECT EXISTS(SELECT 1 FROM "+table+" WHERE token = ?) AS found", nil, tokenstr).Scan(&result).Error; err != nil { 163 if err == gorm.ErrRecordNotFound { 164 return helpers.InvalidTokenError(e) 165 } 166 167 - s.logger.Error("error getting token from db", "error", err) 168 return helpers.ServerError(e, nil) 169 } 170 ··· 175 176 exp, ok := claims["exp"].(float64) 177 if !ok { 178 - s.logger.Error("error getting iat from token") 179 return helpers.ServerError(e, nil) 180 } 181 ··· 184 } 185 186 if repo == nil { 187 - maybeRepo, err := s.getRepoActorByDid(claims["sub"].(string)) 188 if err != nil { 189 - s.logger.Error("error fetching repo", "error", err) 190 return helpers.ServerError(e, nil) 191 } 192 repo = maybeRepo ··· 207 208 func (s *Server) handleOauthSessionMiddleware(next echo.HandlerFunc) echo.HandlerFunc { 209 return func(e echo.Context) error { 210 authheader := e.Request().Header.Get("authorization") 211 if authheader == "" { 212 return e.JSON(401, map[string]string{"error": "Unauthorized"}) ··· 232 proof, err := s.oauthProvider.DpopManager.CheckProof(e.Request().Method, "https://"+s.config.Hostname+e.Request().URL.String(), e.Request().Header, to.StringPtr(accessToken)) 233 if err != nil { 234 if errors.Is(err, dpop.ErrUseDpopNonce) { 235 - return e.JSON(400, map[string]string{ 236 "error": "use_dpop_nonce", 237 }) 238 } 239 - s.logger.Error("invalid dpop proof", "error", err) 240 return helpers.InputError(e, nil) 241 } 242 243 var oauthToken provider.OauthToken 244 - if err := s.db.Raw("SELECT * FROM oauth_tokens WHERE token = ?", nil, accessToken).Scan(&oauthToken).Error; err != nil { 245 - s.logger.Error("error finding access token in db", "error", err) 246 return helpers.InputError(e, nil) 247 } 248 ··· 251 } 252 253 if *oauthToken.Parameters.DpopJkt != proof.JKT { 254 - s.logger.Error("jkt mismatch", "token", oauthToken.Parameters.DpopJkt, "proof", proof.JKT) 255 return helpers.InputError(e, to.StringPtr("dpop jkt mismatch")) 256 } 257 258 if time.Now().After(oauthToken.ExpiresAt) { 259 - return helpers.ExpiredTokenError(e) 260 } 261 262 - repo, err := s.getRepoActorByDid(oauthToken.Sub) 263 if err != nil { 264 - s.logger.Error("could not find actor in db", "error", err) 265 return helpers.ServerError(e, nil) 266 } 267
··· 37 38 func (s *Server) handleLegacySessionMiddleware(next echo.HandlerFunc) echo.HandlerFunc { 39 return func(e echo.Context) error { 40 + ctx := e.Request().Context() 41 + logger := s.logger.With("name", "handleLegacySessionMiddleware") 42 + 43 authheader := e.Request().Header.Get("authorization") 44 if authheader == "" { 45 return e.JSON(401, map[string]string{"error": "Unauthorized"}) ··· 70 if hasLxm { 71 pts := strings.Split(e.Request().URL.String(), "/") 72 if lxm != pts[len(pts)-1] { 73 + logger.Error("service auth lxm incorrect", "lxm", lxm, "expected", pts[len(pts)-1], "error", err) 74 return helpers.InputError(e, nil) 75 } 76 77 maybeDid, ok := claims["iss"].(string) 78 if !ok { 79 + logger.Error("no iss in service auth token", "error", err) 80 return helpers.InputError(e, nil) 81 } 82 did = maybeDid 83 84 + maybeRepo, err := s.getRepoActorByDid(ctx, did) 85 if err != nil { 86 + logger.Error("error fetching repo", "error", err) 87 return helpers.ServerError(e, nil) 88 } 89 repo = maybeRepo ··· 97 return s.privateKey.Public(), nil 98 }) 99 if err != nil { 100 + logger.Error("error parsing jwt", "error", err) 101 return helpers.ExpiredTokenError(e) 102 } 103 ··· 110 hash := sha256.Sum256([]byte(signingInput)) 111 sigBytes, err := base64.RawURLEncoding.DecodeString(kpts[2]) 112 if err != nil { 113 + logger.Error("error decoding signature bytes", "error", err) 114 return helpers.ServerError(e, nil) 115 } 116 117 if len(sigBytes) != 64 { 118 + logger.Error("incorrect sigbytes length", "length", len(sigBytes)) 119 return helpers.ServerError(e, nil) 120 } 121 ··· 124 rr, _ := secp256k1.NewScalarFromBytes((*[32]byte)(rBytes)) 125 ss, _ := secp256k1.NewScalarFromBytes((*[32]byte)(sBytes)) 126 127 + if repo == nil { 128 + sub, ok := claims["sub"].(string) 129 + if !ok { 130 + s.logger.Error("no sub claim in ES256K token and repo not set") 131 + return helpers.InvalidTokenError(e) 132 + } 133 + maybeRepo, err := s.getRepoActorByDid(ctx, sub) 134 + if err != nil { 135 + s.logger.Error("error fetching repo for ES256K verification", "error", err) 136 + return helpers.ServerError(e, nil) 137 + } 138 + repo = maybeRepo 139 + did = sub 140 + } 141 + 142 sk, err := secp256k1secec.NewPrivateKey(repo.SigningKey) 143 if err != nil { 144 + logger.Error("can't load private key", "error", err) 145 return err 146 } 147 148 pubKey, ok := sk.Public().(*secp256k1secec.PublicKey) 149 if !ok { 150 + logger.Error("error getting public key from sk") 151 return helpers.ServerError(e, nil) 152 } 153 154 verified := pubKey.VerifyRaw(hash[:], rr, ss) 155 if !verified { 156 + logger.Error("error verifying", "error", err) 157 return helpers.ServerError(e, nil) 158 } 159 } ··· 177 Found bool 178 } 179 var result Result 180 + if err := s.db.Raw(ctx, "SELECT EXISTS(SELECT 1 FROM "+table+" WHERE token = ?) AS found", nil, tokenstr).Scan(&result).Error; err != nil { 181 if err == gorm.ErrRecordNotFound { 182 return helpers.InvalidTokenError(e) 183 } 184 185 + logger.Error("error getting token from db", "error", err) 186 return helpers.ServerError(e, nil) 187 } 188 ··· 193 194 exp, ok := claims["exp"].(float64) 195 if !ok { 196 + logger.Error("error getting iat from token") 197 return helpers.ServerError(e, nil) 198 } 199 ··· 202 } 203 204 if repo == nil { 205 + maybeRepo, err := s.getRepoActorByDid(ctx, claims["sub"].(string)) 206 if err != nil { 207 + logger.Error("error fetching repo", "error", err) 208 return helpers.ServerError(e, nil) 209 } 210 repo = maybeRepo ··· 225 226 func (s *Server) handleOauthSessionMiddleware(next echo.HandlerFunc) echo.HandlerFunc { 227 return func(e echo.Context) error { 228 + ctx := e.Request().Context() 229 + logger := s.logger.With("name", "handleOauthSessionMiddleware") 230 + 231 authheader := e.Request().Header.Get("authorization") 232 if authheader == "" { 233 return e.JSON(401, map[string]string{"error": "Unauthorized"}) ··· 253 proof, err := s.oauthProvider.DpopManager.CheckProof(e.Request().Method, "https://"+s.config.Hostname+e.Request().URL.String(), e.Request().Header, to.StringPtr(accessToken)) 254 if err != nil { 255 if errors.Is(err, dpop.ErrUseDpopNonce) { 256 + e.Response().Header().Set("WWW-Authenticate", `DPoP error="use_dpop_nonce"`) 257 + e.Response().Header().Add("access-control-expose-headers", "WWW-Authenticate") 258 + return e.JSON(401, map[string]string{ 259 "error": "use_dpop_nonce", 260 }) 261 } 262 + logger.Error("invalid dpop proof", "error", err) 263 return helpers.InputError(e, nil) 264 } 265 266 var oauthToken provider.OauthToken 267 + if err := s.db.Raw(ctx, "SELECT * FROM oauth_tokens WHERE token = ?", nil, accessToken).Scan(&oauthToken).Error; err != nil { 268 + logger.Error("error finding access token in db", "error", err) 269 return helpers.InputError(e, nil) 270 } 271 ··· 274 } 275 276 if *oauthToken.Parameters.DpopJkt != proof.JKT { 277 + logger.Error("jkt mismatch", "token", oauthToken.Parameters.DpopJkt, "proof", proof.JKT) 278 return helpers.InputError(e, to.StringPtr("dpop jkt mismatch")) 279 } 280 281 if time.Now().After(oauthToken.ExpiresAt) { 282 + e.Response().Header().Set("WWW-Authenticate", `DPoP error="invalid_token", error_description="Token expired"`) 283 + e.Response().Header().Add("access-control-expose-headers", "WWW-Authenticate") 284 + return e.JSON(401, map[string]string{ 285 + "error": "invalid_token", 286 + "error_description": "Token expired", 287 + }) 288 } 289 290 + repo, err := s.getRepoActorByDid(ctx, oauthToken.Sub) 291 if err != nil { 292 + logger.Error("could not find actor in db", "error", err) 293 return helpers.ServerError(e, nil) 294 } 295
+85 -32
server/repo.go
··· 17 lexutil "github.com/bluesky-social/indigo/lex/util" 18 "github.com/bluesky-social/indigo/repo" 19 "github.com/haileyok/cocoon/internal/db" 20 "github.com/haileyok/cocoon/models" 21 "github.com/haileyok/cocoon/recording_blockstore" 22 blocks "github.com/ipfs/go-block-format" ··· 96 } 97 98 // TODO make use of swap commit 99 - func (rm *RepoMan) applyWrites(urepo models.Repo, writes []Op, swapCommit *string) ([]ApplyWriteResult, error) { 100 rootcid, err := cid.Cast(urepo.Root) 101 if err != nil { 102 return nil, err ··· 104 105 dbs := rm.s.getBlockstore(urepo.Did) 106 bs := recording_blockstore.New(dbs) 107 - r, err := repo.OpenRepo(context.TODO(), bs, rootcid) 108 109 - entries := []models.Record{} 110 var results []ApplyWriteResult 111 112 for i, op := range writes { 113 if op.Type != OpTypeCreate && op.Rkey == nil { 114 return nil, fmt.Errorf("invalid rkey") 115 } else if op.Type == OpTypeCreate && op.Rkey != nil { 116 - _, _, err := r.GetRecord(context.TODO(), op.Collection+"/"+*op.Rkey) 117 if err == nil { 118 op.Type = OpTypeUpdate 119 } 120 } else if op.Rkey == nil { 121 op.Rkey = to.StringPtr(rm.clock.Next().String()) 122 writes[i].Rkey = op.Rkey 123 } 124 125 _, err := syntax.ParseRecordKey(*op.Rkey) 126 if err != nil { 127 return nil, err ··· 129 130 switch op.Type { 131 case OpTypeCreate: 132 - j, err := json.Marshal(*op.Record) 133 if err != nil { 134 return nil, err 135 } 136 - out, err := atdata.UnmarshalJSON(j) 137 if err != nil { 138 return nil, err 139 } 140 mm := MarshalableMap(out) 141 142 // HACK: if a record doesn't contain a $type, we can manually set it here based on the op's collection 143 if mm["$type"] == "" { 144 mm["$type"] = op.Collection 145 } 146 147 - nc, err := r.PutRecord(context.TODO(), op.Collection+"/"+*op.Rkey, &mm) 148 if err != nil { 149 return nil, err 150 } 151 d, err := atdata.MarshalCBOR(mm) 152 if err != nil { 153 return nil, err 154 } 155 entries = append(entries, models.Record{ 156 Did: urepo.Did, 157 CreatedAt: rm.clock.Next().String(), ··· 160 Cid: nc.String(), 161 Value: d, 162 }) 163 results = append(results, ApplyWriteResult{ 164 Type: to.StringPtr(OpTypeCreate.String()), 165 Uri: to.StringPtr("at://" + urepo.Did + "/" + op.Collection + "/" + *op.Rkey), ··· 167 ValidationStatus: to.StringPtr("valid"), // TODO: obviously this might not be true atm lol 168 }) 169 case OpTypeDelete: 170 var old models.Record 171 - if err := rm.db.Raw("SELECT value FROM records WHERE did = ? AND nsid = ? AND rkey = ?", nil, urepo.Did, op.Collection, op.Rkey).Scan(&old).Error; err != nil { 172 return nil, err 173 } 174 entries = append(entries, models.Record{ 175 Did: urepo.Did, 176 Nsid: op.Collection, 177 Rkey: *op.Rkey, 178 Value: old.Value, 179 }) 180 - err := r.DeleteRecord(context.TODO(), op.Collection+"/"+*op.Rkey) 181 if err != nil { 182 return nil, err 183 } 184 results = append(results, ApplyWriteResult{ 185 Type: to.StringPtr(OpTypeDelete.String()), 186 }) 187 case OpTypeUpdate: 188 - j, err := json.Marshal(*op.Record) 189 if err != nil { 190 return nil, err 191 } 192 - out, err := atdata.UnmarshalJSON(j) 193 if err != nil { 194 return nil, err 195 } 196 mm := MarshalableMap(out) 197 - nc, err := r.UpdateRecord(context.TODO(), op.Collection+"/"+*op.Rkey, &mm) 198 if err != nil { 199 return nil, err 200 } 201 d, err := atdata.MarshalCBOR(mm) 202 if err != nil { 203 return nil, err 204 } 205 entries = append(entries, models.Record{ 206 Did: urepo.Did, 207 CreatedAt: rm.clock.Next().String(), ··· 210 Cid: nc.String(), 211 Value: d, 212 }) 213 results = append(results, ApplyWriteResult{ 214 Type: to.StringPtr(OpTypeUpdate.String()), 215 Uri: to.StringPtr("at://" + urepo.Did + "/" + op.Collection + "/" + *op.Rkey), ··· 219 } 220 } 221 222 - newroot, rev, err := r.Commit(context.TODO(), urepo.SignFor) 223 if err != nil { 224 return nil, err 225 } 226 227 buf := new(bytes.Buffer) 228 229 hb, err := cbor.DumpObject(&car.CarHeader{ 230 Roots: []cid.Cid{newroot}, 231 Version: 1, 232 }) 233 - 234 if _, err := carstore.LdWrite(buf, hb); err != nil { 235 return nil, err 236 } 237 238 - diffops, err := r.DiffSince(context.TODO(), rootcid) 239 if err != nil { 240 return nil, err 241 } 242 243 ops := make([]*atproto.SyncSubscribeRepos_RepoOp, 0, len(diffops)) 244 - 245 for _, op := range diffops { 246 var c cid.Cid 247 switch op.Op { ··· 270 }) 271 } 272 273 - blk, err := dbs.Get(context.TODO(), c) 274 if err != nil { 275 return nil, err 276 } 277 278 if _, err := carstore.LdWrite(buf, blk.Cid().Bytes(), blk.RawData()); err != nil { 279 return nil, err 280 } 281 } 282 283 for _, op := range bs.GetWriteLog() { 284 if _, err := carstore.LdWrite(buf, op.Cid().Bytes(), op.RawData()); err != nil { 285 return nil, err 286 } 287 } 288 289 var blobs []lexutil.LexLink 290 for _, entry := range entries { 291 var cids []cid.Cid 292 if entry.Cid != "" { 293 - if err := rm.s.db.Create(&entry, []clause.Expression{clause.OnConflict{ 294 Columns: []clause.Column{{Name: "did"}, {Name: "nsid"}, {Name: "rkey"}}, 295 UpdateAll: true, 296 }}).Error; err != nil { 297 return nil, err 298 } 299 300 - cids, err = rm.incrementBlobRefs(urepo, entry.Value) 301 if err != nil { 302 return nil, err 303 } 304 } else { 305 - if err := rm.s.db.Delete(&entry, nil).Error; err != nil { 306 return nil, err 307 } 308 - cids, err = rm.decrementBlobRefs(urepo, entry.Value) 309 if err != nil { 310 return nil, err 311 } 312 } 313 314 for _, c := range cids { 315 blobs = append(blobs, lexutil.LexLink(c)) 316 } 317 } 318 319 - rm.s.evtman.AddEvent(context.TODO(), &events.XRPCStreamEvent{ 320 RepoCommit: &atproto.SyncSubscribeRepos_Commit{ 321 Repo: urepo.Did, 322 Blocks: buf.Bytes(), ··· 330 }, 331 }) 332 333 - if err := rm.s.UpdateRepo(context.TODO(), urepo.Did, newroot, rev); err != nil { 334 return nil, err 335 } 336 ··· 345 return results, nil 346 } 347 348 - func (rm *RepoMan) getRecordProof(urepo models.Repo, collection, rkey string) (cid.Cid, []blocks.Block, error) { 349 c, err := cid.Cast(urepo.Root) 350 if err != nil { 351 return cid.Undef, nil, err ··· 354 dbs := rm.s.getBlockstore(urepo.Did) 355 bs := recording_blockstore.New(dbs) 356 357 - r, err := repo.OpenRepo(context.TODO(), bs, c) 358 if err != nil { 359 return cid.Undef, nil, err 360 } 361 362 - _, _, err = r.GetRecordBytes(context.TODO(), collection+"/"+rkey) 363 if err != nil { 364 return cid.Undef, nil, err 365 } ··· 367 return c, bs.GetReadLog(), nil 368 } 369 370 - func (rm *RepoMan) incrementBlobRefs(urepo models.Repo, cbor []byte) ([]cid.Cid, error) { 371 cids, err := getBlobCidsFromCbor(cbor) 372 if err != nil { 373 return nil, err 374 } 375 376 for _, c := range cids { 377 - if err := rm.db.Exec("UPDATE blobs SET ref_count = ref_count + 1 WHERE did = ? AND cid = ?", nil, urepo.Did, c.Bytes()).Error; err != nil { 378 return nil, err 379 } 380 } ··· 382 return cids, nil 383 } 384 385 - func (rm *RepoMan) decrementBlobRefs(urepo models.Repo, cbor []byte) ([]cid.Cid, error) { 386 cids, err := getBlobCidsFromCbor(cbor) 387 if err != nil { 388 return nil, err ··· 393 ID uint 394 Count int 395 } 396 - if err := rm.db.Raw("UPDATE blobs SET ref_count = ref_count - 1 WHERE did = ? AND cid = ? RETURNING id, ref_count", nil, urepo.Did, c.Bytes()).Scan(&res).Error; err != nil { 397 return nil, err 398 } 399 400 if res.Count == 0 { 401 - if err := rm.db.Exec("DELETE FROM blobs WHERE id = ?", nil, res.ID).Error; err != nil { 402 return nil, err 403 } 404 - if err := rm.db.Exec("DELETE FROM blob_parts WHERE blob_id = ?", nil, res.ID).Error; err != nil { 405 return nil, err 406 } 407 }
··· 17 lexutil "github.com/bluesky-social/indigo/lex/util" 18 "github.com/bluesky-social/indigo/repo" 19 "github.com/haileyok/cocoon/internal/db" 20 + "github.com/haileyok/cocoon/metrics" 21 "github.com/haileyok/cocoon/models" 22 "github.com/haileyok/cocoon/recording_blockstore" 23 blocks "github.com/ipfs/go-block-format" ··· 97 } 98 99 // TODO make use of swap commit 100 + func (rm *RepoMan) applyWrites(ctx context.Context, urepo models.Repo, writes []Op, swapCommit *string) ([]ApplyWriteResult, error) { 101 rootcid, err := cid.Cast(urepo.Root) 102 if err != nil { 103 return nil, err ··· 105 106 dbs := rm.s.getBlockstore(urepo.Did) 107 bs := recording_blockstore.New(dbs) 108 + r, err := repo.OpenRepo(ctx, bs, rootcid) 109 110 var results []ApplyWriteResult 111 112 + entries := make([]models.Record, 0, len(writes)) 113 for i, op := range writes { 114 + // updates or deletes must supply an rkey 115 if op.Type != OpTypeCreate && op.Rkey == nil { 116 return nil, fmt.Errorf("invalid rkey") 117 } else if op.Type == OpTypeCreate && op.Rkey != nil { 118 + // we should conver this op to an update if the rkey already exists 119 + _, _, err := r.GetRecord(ctx, fmt.Sprintf("%s/%s", op.Collection, *op.Rkey)) 120 if err == nil { 121 op.Type = OpTypeUpdate 122 } 123 } else if op.Rkey == nil { 124 + // creates that don't supply an rkey will have one generated for them 125 op.Rkey = to.StringPtr(rm.clock.Next().String()) 126 writes[i].Rkey = op.Rkey 127 } 128 129 + // validate the record key is actually valid 130 _, err := syntax.ParseRecordKey(*op.Rkey) 131 if err != nil { 132 return nil, err ··· 134 135 switch op.Type { 136 case OpTypeCreate: 137 + // HACK: this fixes some type conversions, mainly around integers 138 + // first we convert to json bytes 139 + b, err := json.Marshal(*op.Record) 140 if err != nil { 141 return nil, err 142 } 143 + // then we use atdata.UnmarshalJSON to convert it back to a map 144 + out, err := atdata.UnmarshalJSON(b) 145 if err != nil { 146 return nil, err 147 } 148 + // finally we can cast to a MarshalableMap 149 mm := MarshalableMap(out) 150 151 // HACK: if a record doesn't contain a $type, we can manually set it here based on the op's collection 152 + // i forget why this is actually necessary? 153 if mm["$type"] == "" { 154 mm["$type"] = op.Collection 155 } 156 157 + nc, err := r.PutRecord(ctx, fmt.Sprintf("%s/%s", op.Collection, *op.Rkey), &mm) 158 if err != nil { 159 return nil, err 160 } 161 + 162 d, err := atdata.MarshalCBOR(mm) 163 if err != nil { 164 return nil, err 165 } 166 + 167 entries = append(entries, models.Record{ 168 Did: urepo.Did, 169 CreatedAt: rm.clock.Next().String(), ··· 172 Cid: nc.String(), 173 Value: d, 174 }) 175 + 176 results = append(results, ApplyWriteResult{ 177 Type: to.StringPtr(OpTypeCreate.String()), 178 Uri: to.StringPtr("at://" + urepo.Did + "/" + op.Collection + "/" + *op.Rkey), ··· 180 ValidationStatus: to.StringPtr("valid"), // TODO: obviously this might not be true atm lol 181 }) 182 case OpTypeDelete: 183 + // try to find the old record in the database 184 var old models.Record 185 + if err := rm.db.Raw(ctx, "SELECT value FROM records WHERE did = ? AND nsid = ? AND rkey = ?", nil, urepo.Did, op.Collection, op.Rkey).Scan(&old).Error; err != nil { 186 return nil, err 187 } 188 + 189 + // TODO: this is really confusing, and looking at it i have no idea why i did this. below when we are doing deletes, we 190 + // check if `cid` here is nil to indicate if we should delete. that really doesn't make much sense and its super illogical 191 + // when reading this code. i dont feel like fixing right now though so 192 entries = append(entries, models.Record{ 193 Did: urepo.Did, 194 Nsid: op.Collection, 195 Rkey: *op.Rkey, 196 Value: old.Value, 197 }) 198 + 199 + // delete the record from the repo 200 + err := r.DeleteRecord(ctx, fmt.Sprintf("%s/%s", op.Collection, *op.Rkey)) 201 if err != nil { 202 return nil, err 203 } 204 + 205 + // add a result for the delete 206 results = append(results, ApplyWriteResult{ 207 Type: to.StringPtr(OpTypeDelete.String()), 208 }) 209 case OpTypeUpdate: 210 + // HACK: same hack as above for type fixes 211 + b, err := json.Marshal(*op.Record) 212 if err != nil { 213 return nil, err 214 } 215 + out, err := atdata.UnmarshalJSON(b) 216 if err != nil { 217 return nil, err 218 } 219 mm := MarshalableMap(out) 220 + 221 + nc, err := r.UpdateRecord(ctx, fmt.Sprintf("%s/%s", op.Collection, *op.Rkey), &mm) 222 if err != nil { 223 return nil, err 224 } 225 + 226 d, err := atdata.MarshalCBOR(mm) 227 if err != nil { 228 return nil, err 229 } 230 + 231 entries = append(entries, models.Record{ 232 Did: urepo.Did, 233 CreatedAt: rm.clock.Next().String(), ··· 236 Cid: nc.String(), 237 Value: d, 238 }) 239 + 240 results = append(results, ApplyWriteResult{ 241 Type: to.StringPtr(OpTypeUpdate.String()), 242 Uri: to.StringPtr("at://" + urepo.Did + "/" + op.Collection + "/" + *op.Rkey), ··· 246 } 247 } 248 249 + // commit and get the new root 250 + newroot, rev, err := r.Commit(ctx, urepo.SignFor) 251 if err != nil { 252 return nil, err 253 } 254 255 + for _, result := range results { 256 + if result.Type != nil { 257 + metrics.RepoOperations.WithLabelValues(*result.Type).Inc() 258 + } 259 + } 260 + 261 + // create a buffer for dumping our new cbor into 262 buf := new(bytes.Buffer) 263 264 + // first write the car header to the buffer 265 hb, err := cbor.DumpObject(&car.CarHeader{ 266 Roots: []cid.Cid{newroot}, 267 Version: 1, 268 }) 269 if _, err := carstore.LdWrite(buf, hb); err != nil { 270 return nil, err 271 } 272 273 + // get a diff of the changes to the repo 274 + diffops, err := r.DiffSince(ctx, rootcid) 275 if err != nil { 276 return nil, err 277 } 278 279 + // create the repo ops for the given diff 280 ops := make([]*atproto.SyncSubscribeRepos_RepoOp, 0, len(diffops)) 281 for _, op := range diffops { 282 var c cid.Cid 283 switch op.Op { ··· 306 }) 307 } 308 309 + blk, err := dbs.Get(ctx, c) 310 if err != nil { 311 return nil, err 312 } 313 314 + // write the block to the buffer 315 if _, err := carstore.LdWrite(buf, blk.Cid().Bytes(), blk.RawData()); err != nil { 316 return nil, err 317 } 318 } 319 320 + // write the writelog to the buffer 321 for _, op := range bs.GetWriteLog() { 322 if _, err := carstore.LdWrite(buf, op.Cid().Bytes(), op.RawData()); err != nil { 323 return nil, err 324 } 325 } 326 327 + // blob blob blob blob blob :3 328 var blobs []lexutil.LexLink 329 for _, entry := range entries { 330 var cids []cid.Cid 331 + // whenever there is cid present, we know it's a create (dumb) 332 if entry.Cid != "" { 333 + if err := rm.s.db.Create(ctx, &entry, []clause.Expression{clause.OnConflict{ 334 Columns: []clause.Column{{Name: "did"}, {Name: "nsid"}, {Name: "rkey"}}, 335 UpdateAll: true, 336 }}).Error; err != nil { 337 return nil, err 338 } 339 340 + // increment the given blob refs, yay 341 + cids, err = rm.incrementBlobRefs(ctx, urepo, entry.Value) 342 if err != nil { 343 return nil, err 344 } 345 } else { 346 + // as i noted above this is dumb. but we delete whenever the cid is nil. it works solely becaue the pkey 347 + // is did + collection + rkey. i still really want to separate that out, or use a different type to make 348 + // this less confusing/easy to read. alas, its 2 am and yea no 349 + if err := rm.s.db.Delete(ctx, &entry, nil).Error; err != nil { 350 return nil, err 351 } 352 + 353 + // TODO: 354 + cids, err = rm.decrementBlobRefs(ctx, urepo, entry.Value) 355 if err != nil { 356 return nil, err 357 } 358 } 359 360 + // add all the relevant blobs to the blobs list of blobs. blob ^.^ 361 for _, c := range cids { 362 blobs = append(blobs, lexutil.LexLink(c)) 363 } 364 } 365 366 + // NOTE: using the request ctx seems a bit suss here, so using a background context. i'm not sure if this 367 + // runs sync or not 368 + rm.s.evtman.AddEvent(context.Background(), &events.XRPCStreamEvent{ 369 RepoCommit: &atproto.SyncSubscribeRepos_Commit{ 370 Repo: urepo.Did, 371 Blocks: buf.Bytes(), ··· 379 }, 380 }) 381 382 + if err := rm.s.UpdateRepo(ctx, urepo.Did, newroot, rev); err != nil { 383 return nil, err 384 } 385 ··· 394 return results, nil 395 } 396 397 + // this is a fun little guy. to get a proof, we need to read the record out of the blockstore and record how we actually 398 + // got to the guy. we'll wrap a new blockstore in a recording blockstore, then return the log for proof 399 + func (rm *RepoMan) getRecordProof(ctx context.Context, urepo models.Repo, collection, rkey string) (cid.Cid, []blocks.Block, error) { 400 c, err := cid.Cast(urepo.Root) 401 if err != nil { 402 return cid.Undef, nil, err ··· 405 dbs := rm.s.getBlockstore(urepo.Did) 406 bs := recording_blockstore.New(dbs) 407 408 + r, err := repo.OpenRepo(ctx, bs, c) 409 if err != nil { 410 return cid.Undef, nil, err 411 } 412 413 + _, _, err = r.GetRecordBytes(ctx, fmt.Sprintf("%s/%s", collection, rkey)) 414 if err != nil { 415 return cid.Undef, nil, err 416 } ··· 418 return c, bs.GetReadLog(), nil 419 } 420 421 + func (rm *RepoMan) incrementBlobRefs(ctx context.Context, urepo models.Repo, cbor []byte) ([]cid.Cid, error) { 422 cids, err := getBlobCidsFromCbor(cbor) 423 if err != nil { 424 return nil, err 425 } 426 427 for _, c := range cids { 428 + if err := rm.db.Exec(ctx, "UPDATE blobs SET ref_count = ref_count + 1 WHERE did = ? AND cid = ?", nil, urepo.Did, c.Bytes()).Error; err != nil { 429 return nil, err 430 } 431 } ··· 433 return cids, nil 434 } 435 436 + func (rm *RepoMan) decrementBlobRefs(ctx context.Context, urepo models.Repo, cbor []byte) ([]cid.Cid, error) { 437 cids, err := getBlobCidsFromCbor(cbor) 438 if err != nil { 439 return nil, err ··· 444 ID uint 445 Count int 446 } 447 + if err := rm.db.Raw(ctx, "UPDATE blobs SET ref_count = ref_count - 1 WHERE did = ? AND cid = ? RETURNING id, ref_count", nil, urepo.Did, c.Bytes()).Scan(&res).Error; err != nil { 448 return nil, err 449 } 450 451 + // TODO: this does _not_ handle deletions of blobs that are on s3 storage!!!! we need to get the blob, see what 452 + // storage it is in, and clean up s3!!!! 453 if res.Count == 0 { 454 + if err := rm.db.Exec(ctx, "DELETE FROM blobs WHERE id = ?", nil, res.ID).Error; err != nil { 455 return nil, err 456 } 457 + if err := rm.db.Exec(ctx, "DELETE FROM blob_parts WHERE blob_id = ?", nil, res.ID).Error; err != nil { 458 return nil, err 459 } 460 }
+82 -28
server/server.go
··· 39 "github.com/haileyok/cocoon/oauth/provider" 40 "github.com/haileyok/cocoon/plc" 41 "github.com/ipfs/go-cid" 42 echo_session "github.com/labstack/echo-contrib/session" 43 "github.com/labstack/echo/v4" 44 "github.com/labstack/echo/v4/middleware" 45 slogecho "github.com/samber/slog-echo" 46 "gorm.io/driver/sqlite" 47 "gorm.io/gorm" 48 ) ··· 59 Bucket string 60 AccessKey string 61 SecretKey string 62 } 63 64 type Server struct { ··· 82 requestCrawlMu sync.Mutex 83 84 dbName string 85 s3Config *S3Config 86 } 87 88 type Args struct { 89 Addr string 90 DbName string 91 - Logger *slog.Logger 92 Version string 93 Did string 94 Hostname string ··· 97 ContactEmail string 98 Relays []string 99 AdminPassword string 100 101 SmtpUser string 102 SmtpPass string ··· 121 EnforcePeering bool 122 Relays []string 123 AdminPassword string 124 SmtpEmail string 125 SmtpName string 126 BlockstoreVariant BlockstoreVariant ··· 202 } 203 204 func New(args *Args) (*Server, error) { 205 if args.Addr == "" { 206 return nil, fmt.Errorf("addr must be set") 207 } ··· 230 return nil, fmt.Errorf("admin password must be set") 231 } 232 233 - if args.Logger == nil { 234 - args.Logger = slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{})) 235 - } 236 - 237 if args.SessionSecret == "" { 238 panic("SESSION SECRET WAS NOT SET. THIS IS REQUIRED. ") 239 } ··· 241 e := echo.New() 242 243 e.Pre(middleware.RemoveTrailingSlash()) 244 - e.Pre(slogecho.New(args.Logger)) 245 e.Use(echo_session.Middleware(sessions.NewCookieStore([]byte(args.SessionSecret)))) 246 e.Use(middleware.CORSWithConfig(middleware.CORSConfig{ 247 AllowOrigins: []string{"*"}, 248 AllowHeaders: []string{"*"}, ··· 288 IdleTimeout: 5 * time.Minute, 289 } 290 291 - gdb, err := gorm.Open(sqlite.Open(args.DbName), &gorm.Config{}) 292 - if err != nil { 293 - return nil, err 294 } 295 dbw := db.NewDB(gdb) 296 ··· 333 var nonceSecret []byte 334 maybeSecret, err := os.ReadFile("nonce.secret") 335 if err != nil && !os.IsNotExist(err) { 336 - args.Logger.Error("error attempting to read nonce secret", "error", err) 337 } else { 338 nonceSecret = maybeSecret 339 } ··· 354 EnforcePeering: false, 355 Relays: args.Relays, 356 AdminPassword: args.AdminPassword, 357 SmtpName: args.SmtpName, 358 SmtpEmail: args.SmtpEmail, 359 BlockstoreVariant: args.BlockstoreVariant, ··· 363 passport: identity.NewPassport(h, identity.NewMemCache(10_000)), 364 365 dbName: args.DbName, 366 s3Config: args.S3Config, 367 368 oauthProvider: provider.NewProvider(provider.Args{ 369 Hostname: args.Hostname, 370 ClientManagerArgs: client.ManagerArgs{ 371 Cli: oauthCli, 372 - Logger: args.Logger, 373 }, 374 DpopManagerArgs: dpop.ManagerArgs{ 375 NonceSecret: nonceSecret, 376 NonceRotationInterval: constants.NonceMaxRotationInterval / 3, 377 OnNonceSecretCreated: func(newNonce []byte) { 378 if err := os.WriteFile("nonce.secret", newNonce, 0644); err != nil { 379 - args.Logger.Error("error writing new nonce secret", "error", err) 380 } 381 }, 382 - Logger: args.Logger, 383 Hostname: args.Hostname, 384 }, 385 }), ··· 416 s.echo.GET("/", s.handleRoot) 417 s.echo.GET("/xrpc/_health", s.handleHealth) 418 s.echo.GET("/.well-known/did.json", s.handleWellKnown) 419 s.echo.GET("/.well-known/oauth-protected-resource", s.handleOauthProtectedResource) 420 s.echo.GET("/.well-known/oauth-authorization-server", s.handleOauthAuthorizationServer) 421 s.echo.GET("/robots.txt", s.handleRobots) ··· 425 s.echo.POST("/xrpc/com.atproto.server.createAccount", s.handleCreateAccount) 426 s.echo.POST("/xrpc/com.atproto.server.createSession", s.handleCreateSession) 427 s.echo.GET("/xrpc/com.atproto.server.describeServer", s.handleDescribeServer) 428 429 s.echo.GET("/xrpc/com.atproto.repo.describeRepo", s.handleDescribeRepo) 430 s.echo.GET("/xrpc/com.atproto.sync.listRepos", s.handleListRepos) ··· 439 s.echo.GET("/xrpc/com.atproto.sync.listBlobs", s.handleSyncListBlobs) 440 s.echo.GET("/xrpc/com.atproto.sync.getBlob", s.handleSyncGetBlob) 441 442 // account 443 s.echo.GET("/account", s.handleAccount) 444 s.echo.POST("/account/revoke", s.handleAccountRevoke) ··· 474 s.echo.GET("/xrpc/com.atproto.server.checkAccountStatus", s.handleServerCheckAccountStatus, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware) 475 s.echo.POST("/xrpc/com.atproto.server.deactivateAccount", s.handleServerDeactivateAccount, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware) 476 s.echo.POST("/xrpc/com.atproto.server.activateAccount", s.handleServerActivateAccount, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware) 477 478 // repo 479 s.echo.POST("/xrpc/com.atproto.repo.createRecord", s.handleCreateRecord, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware) 480 s.echo.POST("/xrpc/com.atproto.repo.putRecord", s.handlePutRecord, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware) 481 s.echo.POST("/xrpc/com.atproto.repo.deleteRecord", s.handleDeleteRecord, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware) ··· 498 } 499 500 func (s *Server) Serve(ctx context.Context) error { 501 s.addRoutes() 502 503 - s.logger.Info("migrating...") 504 505 s.db.AutoMigrate( 506 &models.Actor{}, ··· 512 &models.Record{}, 513 &models.Blob{}, 514 &models.BlobPart{}, 515 &provider.OauthToken{}, 516 &provider.OauthAuthorizationRequest{}, 517 ) 518 519 - s.logger.Info("starting cocoon") 520 521 go func() { 522 if err := s.httpd.ListenAndServe(); err != nil { ··· 528 529 go func() { 530 if err := s.requestCrawl(ctx); err != nil { 531 - s.logger.Error("error requesting crawls", "err", err) 532 } 533 }() 534 ··· 546 547 logger.Info("requesting crawl with configured relays") 548 549 - if time.Now().Sub(s.lastRequestCrawl) <= 1*time.Minute { 550 return fmt.Errorf("a crawl request has already been made within the last minute") 551 } 552 ··· 569 } 570 571 func (s *Server) doBackup() { 572 start := time.Now() 573 574 - s.logger.Info("beginning backup to s3...") 575 576 var buf bytes.Buffer 577 if err := func() error { 578 - s.logger.Info("reading database bytes...") 579 s.db.Lock() 580 defer s.db.Unlock() 581 ··· 591 592 return nil 593 }(); err != nil { 594 - s.logger.Error("error backing up database", "error", err) 595 return 596 } 597 598 if err := func() error { 599 - s.logger.Info("sending to s3...") 600 601 currTime := time.Now().Format("2006-01-02_15-04-05") 602 key := "cocoon-backup-" + currTime + ".db" ··· 626 return fmt.Errorf("error uploading file to s3: %w", err) 627 } 628 629 - s.logger.Info("finished uploading backup to s3", "key", key, "duration", time.Now().Sub(start).Seconds()) 630 631 return nil 632 }(); err != nil { 633 - s.logger.Error("error uploading database backup", "error", err) 634 return 635 } 636 ··· 638 } 639 640 func (s *Server) backupRoutine() { 641 if s.s3Config == nil || !s.s3Config.BackupsEnabled { 642 return 643 } 644 645 if s.s3Config.Region == "" { 646 - s.logger.Warn("no s3 region configured but backups are enabled. backups will not run.") 647 return 648 } 649 650 if s.s3Config.Bucket == "" { 651 - s.logger.Warn("no s3 bucket configured but backups are enabled. backups will not run.") 652 return 653 } 654 655 if s.s3Config.AccessKey == "" { 656 - s.logger.Warn("no s3 access key configured but backups are enabled. backups will not run.") 657 return 658 } 659 660 if s.s3Config.SecretKey == "" { 661 - s.logger.Warn("no s3 secret key configured but backups are enabled. backups will not run.") 662 return 663 } 664 ··· 686 } 687 688 func (s *Server) UpdateRepo(ctx context.Context, did string, root cid.Cid, rev string) error { 689 - if err := s.db.Exec("UPDATE repos SET root = ?, rev = ? WHERE did = ?", nil, root.Bytes(), rev, did).Error; err != nil { 690 return err 691 } 692
··· 39 "github.com/haileyok/cocoon/oauth/provider" 40 "github.com/haileyok/cocoon/plc" 41 "github.com/ipfs/go-cid" 42 + "github.com/labstack/echo-contrib/echoprometheus" 43 echo_session "github.com/labstack/echo-contrib/session" 44 "github.com/labstack/echo/v4" 45 "github.com/labstack/echo/v4/middleware" 46 slogecho "github.com/samber/slog-echo" 47 + "gorm.io/driver/postgres" 48 "gorm.io/driver/sqlite" 49 "gorm.io/gorm" 50 ) ··· 61 Bucket string 62 AccessKey string 63 SecretKey string 64 + CDNUrl string 65 } 66 67 type Server struct { ··· 85 requestCrawlMu sync.Mutex 86 87 dbName string 88 + dbType string 89 s3Config *S3Config 90 } 91 92 type Args struct { 93 + Logger *slog.Logger 94 + 95 Addr string 96 DbName string 97 + DbType string 98 + DatabaseURL string 99 Version string 100 Did string 101 Hostname string ··· 104 ContactEmail string 105 Relays []string 106 AdminPassword string 107 + RequireInvite bool 108 109 SmtpUser string 110 SmtpPass string ··· 129 EnforcePeering bool 130 Relays []string 131 AdminPassword string 132 + RequireInvite bool 133 SmtpEmail string 134 SmtpName string 135 BlockstoreVariant BlockstoreVariant ··· 211 } 212 213 func New(args *Args) (*Server, error) { 214 + if args.Logger == nil { 215 + args.Logger = slog.Default() 216 + } 217 + 218 + logger := args.Logger.With("name", "New") 219 + 220 if args.Addr == "" { 221 return nil, fmt.Errorf("addr must be set") 222 } ··· 245 return nil, fmt.Errorf("admin password must be set") 246 } 247 248 if args.SessionSecret == "" { 249 panic("SESSION SECRET WAS NOT SET. THIS IS REQUIRED. ") 250 } ··· 252 e := echo.New() 253 254 e.Pre(middleware.RemoveTrailingSlash()) 255 + e.Pre(slogecho.New(args.Logger.With("component", "slogecho"))) 256 e.Use(echo_session.Middleware(sessions.NewCookieStore([]byte(args.SessionSecret)))) 257 + e.Use(echoprometheus.NewMiddleware("cocoon")) 258 e.Use(middleware.CORSWithConfig(middleware.CORSConfig{ 259 AllowOrigins: []string{"*"}, 260 AllowHeaders: []string{"*"}, ··· 300 IdleTimeout: 5 * time.Minute, 301 } 302 303 + dbType := args.DbType 304 + if dbType == "" { 305 + dbType = "sqlite" 306 + } 307 + 308 + var gdb *gorm.DB 309 + var err error 310 + switch dbType { 311 + case "postgres": 312 + if args.DatabaseURL == "" { 313 + return nil, fmt.Errorf("database-url must be set when using postgres") 314 + } 315 + gdb, err = gorm.Open(postgres.Open(args.DatabaseURL), &gorm.Config{}) 316 + if err != nil { 317 + return nil, fmt.Errorf("failed to connect to postgres: %w", err) 318 + } 319 + logger.Info("connected to PostgreSQL database") 320 + default: 321 + gdb, err = gorm.Open(sqlite.Open(args.DbName), &gorm.Config{}) 322 + if err != nil { 323 + return nil, fmt.Errorf("failed to open sqlite database: %w", err) 324 + } 325 + logger.Info("connected to SQLite database", "path", args.DbName) 326 } 327 dbw := db.NewDB(gdb) 328 ··· 365 var nonceSecret []byte 366 maybeSecret, err := os.ReadFile("nonce.secret") 367 if err != nil && !os.IsNotExist(err) { 368 + logger.Error("error attempting to read nonce secret", "error", err) 369 } else { 370 nonceSecret = maybeSecret 371 } ··· 386 EnforcePeering: false, 387 Relays: args.Relays, 388 AdminPassword: args.AdminPassword, 389 + RequireInvite: args.RequireInvite, 390 SmtpName: args.SmtpName, 391 SmtpEmail: args.SmtpEmail, 392 BlockstoreVariant: args.BlockstoreVariant, ··· 396 passport: identity.NewPassport(h, identity.NewMemCache(10_000)), 397 398 dbName: args.DbName, 399 + dbType: dbType, 400 s3Config: args.S3Config, 401 402 oauthProvider: provider.NewProvider(provider.Args{ 403 Hostname: args.Hostname, 404 ClientManagerArgs: client.ManagerArgs{ 405 Cli: oauthCli, 406 + Logger: args.Logger.With("component", "oauth-client-manager"), 407 }, 408 DpopManagerArgs: dpop.ManagerArgs{ 409 NonceSecret: nonceSecret, 410 NonceRotationInterval: constants.NonceMaxRotationInterval / 3, 411 OnNonceSecretCreated: func(newNonce []byte) { 412 if err := os.WriteFile("nonce.secret", newNonce, 0644); err != nil { 413 + logger.Error("error writing new nonce secret", "error", err) 414 } 415 }, 416 + Logger: args.Logger.With("component", "dpop-manager"), 417 Hostname: args.Hostname, 418 }, 419 }), ··· 450 s.echo.GET("/", s.handleRoot) 451 s.echo.GET("/xrpc/_health", s.handleHealth) 452 s.echo.GET("/.well-known/did.json", s.handleWellKnown) 453 + s.echo.GET("/.well-known/atproto-did", s.handleAtprotoDid) 454 s.echo.GET("/.well-known/oauth-protected-resource", s.handleOauthProtectedResource) 455 s.echo.GET("/.well-known/oauth-authorization-server", s.handleOauthAuthorizationServer) 456 s.echo.GET("/robots.txt", s.handleRobots) ··· 460 s.echo.POST("/xrpc/com.atproto.server.createAccount", s.handleCreateAccount) 461 s.echo.POST("/xrpc/com.atproto.server.createSession", s.handleCreateSession) 462 s.echo.GET("/xrpc/com.atproto.server.describeServer", s.handleDescribeServer) 463 + s.echo.POST("/xrpc/com.atproto.server.reserveSigningKey", s.handleServerReserveSigningKey) 464 465 s.echo.GET("/xrpc/com.atproto.repo.describeRepo", s.handleDescribeRepo) 466 s.echo.GET("/xrpc/com.atproto.sync.listRepos", s.handleListRepos) ··· 475 s.echo.GET("/xrpc/com.atproto.sync.listBlobs", s.handleSyncListBlobs) 476 s.echo.GET("/xrpc/com.atproto.sync.getBlob", s.handleSyncGetBlob) 477 478 + // labels 479 + s.echo.GET("/xrpc/com.atproto.label.queryLabels", s.handleLabelQueryLabels) 480 + 481 // account 482 s.echo.GET("/account", s.handleAccount) 483 s.echo.POST("/account/revoke", s.handleAccountRevoke) ··· 513 s.echo.GET("/xrpc/com.atproto.server.checkAccountStatus", s.handleServerCheckAccountStatus, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware) 514 s.echo.POST("/xrpc/com.atproto.server.deactivateAccount", s.handleServerDeactivateAccount, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware) 515 s.echo.POST("/xrpc/com.atproto.server.activateAccount", s.handleServerActivateAccount, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware) 516 + s.echo.POST("/xrpc/com.atproto.server.requestAccountDelete", s.handleServerRequestAccountDelete, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware) 517 + s.echo.POST("/xrpc/com.atproto.server.deleteAccount", s.handleServerDeleteAccount) 518 519 // repo 520 + s.echo.GET("/xrpc/com.atproto.repo.listMissingBlobs", s.handleListMissingBlobs, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware) 521 s.echo.POST("/xrpc/com.atproto.repo.createRecord", s.handleCreateRecord, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware) 522 s.echo.POST("/xrpc/com.atproto.repo.putRecord", s.handlePutRecord, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware) 523 s.echo.POST("/xrpc/com.atproto.repo.deleteRecord", s.handleDeleteRecord, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware) ··· 540 } 541 542 func (s *Server) Serve(ctx context.Context) error { 543 + logger := s.logger.With("name", "Serve") 544 + 545 s.addRoutes() 546 547 + logger.Info("migrating...") 548 549 s.db.AutoMigrate( 550 &models.Actor{}, ··· 556 &models.Record{}, 557 &models.Blob{}, 558 &models.BlobPart{}, 559 + &models.ReservedKey{}, 560 &provider.OauthToken{}, 561 &provider.OauthAuthorizationRequest{}, 562 ) 563 564 + logger.Info("starting cocoon") 565 566 go func() { 567 if err := s.httpd.ListenAndServe(); err != nil { ··· 573 574 go func() { 575 if err := s.requestCrawl(ctx); err != nil { 576 + logger.Error("error requesting crawls", "err", err) 577 } 578 }() 579 ··· 591 592 logger.Info("requesting crawl with configured relays") 593 594 + if time.Since(s.lastRequestCrawl) <= 1*time.Minute { 595 return fmt.Errorf("a crawl request has already been made within the last minute") 596 } 597 ··· 614 } 615 616 func (s *Server) doBackup() { 617 + logger := s.logger.With("name", "doBackup") 618 + 619 + if s.dbType == "postgres" { 620 + logger.Info("skipping S3 backup - PostgreSQL backups should be handled externally (pg_dump, managed database backups, etc.)") 621 + return 622 + } 623 + 624 start := time.Now() 625 626 + logger.Info("beginning backup to s3...") 627 628 var buf bytes.Buffer 629 if err := func() error { 630 + logger.Info("reading database bytes...") 631 s.db.Lock() 632 defer s.db.Unlock() 633 ··· 643 644 return nil 645 }(); err != nil { 646 + logger.Error("error backing up database", "error", err) 647 return 648 } 649 650 if err := func() error { 651 + logger.Info("sending to s3...") 652 653 currTime := time.Now().Format("2006-01-02_15-04-05") 654 key := "cocoon-backup-" + currTime + ".db" ··· 678 return fmt.Errorf("error uploading file to s3: %w", err) 679 } 680 681 + logger.Info("finished uploading backup to s3", "key", key, "duration", time.Now().Sub(start).Seconds()) 682 683 return nil 684 }(); err != nil { 685 + logger.Error("error uploading database backup", "error", err) 686 return 687 } 688 ··· 690 } 691 692 func (s *Server) backupRoutine() { 693 + logger := s.logger.With("name", "backupRoutine") 694 + 695 if s.s3Config == nil || !s.s3Config.BackupsEnabled { 696 return 697 } 698 699 if s.s3Config.Region == "" { 700 + logger.Warn("no s3 region configured but backups are enabled. backups will not run.") 701 return 702 } 703 704 if s.s3Config.Bucket == "" { 705 + logger.Warn("no s3 bucket configured but backups are enabled. backups will not run.") 706 return 707 } 708 709 if s.s3Config.AccessKey == "" { 710 + logger.Warn("no s3 access key configured but backups are enabled. backups will not run.") 711 return 712 } 713 714 if s.s3Config.SecretKey == "" { 715 + logger.Warn("no s3 secret key configured but backups are enabled. backups will not run.") 716 return 717 } 718 ··· 740 } 741 742 func (s *Server) UpdateRepo(ctx context.Context, did string, root cid.Cid, rev string) error { 743 + if err := s.db.Exec(ctx, "UPDATE repos SET root = ?, rev = ? WHERE did = ?", nil, root.Bytes(), rev, did).Error; err != nil { 744 return err 745 } 746
+4 -3
server/session.go
··· 1 package server 2 3 import ( 4 "time" 5 6 "github.com/golang-jwt/jwt/v4" ··· 13 RefreshToken string 14 } 15 16 - func (s *Server) createSession(repo *models.Repo) (*Session, error) { 17 now := time.Now() 18 accexp := now.Add(3 * time.Hour) 19 refexp := now.Add(7 * 24 * time.Hour) ··· 49 return nil, err 50 } 51 52 - if err := s.db.Create(&models.Token{ 53 Token: accessString, 54 Did: repo.Did, 55 RefreshToken: refreshString, ··· 59 return nil, err 60 } 61 62 - if err := s.db.Create(&models.RefreshToken{ 63 Token: refreshString, 64 Did: repo.Did, 65 CreatedAt: now,
··· 1 package server 2 3 import ( 4 + "context" 5 "time" 6 7 "github.com/golang-jwt/jwt/v4" ··· 14 RefreshToken string 15 } 16 17 + func (s *Server) createSession(ctx context.Context, repo *models.Repo) (*Session, error) { 18 now := time.Now() 19 accexp := now.Add(3 * time.Hour) 20 refexp := now.Add(7 * 24 * time.Hour) ··· 50 return nil, err 51 } 52 53 + if err := s.db.Create(ctx, &models.Token{ 54 Token: accessString, 55 Did: repo.Did, 56 RefreshToken: refreshString, ··· 60 return nil, err 61 } 62 63 + if err := s.db.Create(ctx, &models.RefreshToken{ 64 Token: refreshString, 65 Did: repo.Did, 66 CreatedAt: now,
+4
server/templates/signin.html
··· 26 type="password" 27 placeholder="Password" 28 /> 29 <input name="query_params" type="hidden" value="{{ .QueryParams }}" /> 30 <button class="primary" type="submit" value="Login">Login</button> 31 </form>
··· 26 type="password" 27 placeholder="Password" 28 /> 29 + {{ if .flashes.tokenrequired }} 30 + <br /> 31 + <input name="token" id="token" placeholder="Enter your 2FA token" /> 32 + {{ end }} 33 <input name="query_params" type="hidden" value="{{ .QueryParams }}" /> 34 <button class="primary" type="submit" value="Login">Login</button> 35 </form>
+3 -3
sqlite_blockstore/sqlite_blockstore.go
··· 45 return maybeBlock, nil 46 } 47 48 - if err := bs.db.Raw("SELECT * FROM blocks WHERE did = ? AND cid = ?", nil, bs.did, cid.Bytes()).Scan(&block).Error; err != nil { 49 return nil, err 50 } 51 ··· 71 Value: block.RawData(), 72 } 73 74 - if err := bs.db.Create(&b, []clause.Expression{clause.OnConflict{ 75 Columns: []clause.Column{{Name: "did"}, {Name: "cid"}}, 76 UpdateAll: true, 77 }}).Error; err != nil { ··· 94 } 95 96 func (bs *SqliteBlockstore) PutMany(ctx context.Context, blocks []blocks.Block) error { 97 - tx := bs.db.BeginDangerously() 98 99 for _, block := range blocks { 100 bs.inserts[block.Cid()] = block
··· 45 return maybeBlock, nil 46 } 47 48 + if err := bs.db.Raw(ctx, "SELECT * FROM blocks WHERE did = ? AND cid = ?", nil, bs.did, cid.Bytes()).Scan(&block).Error; err != nil { 49 return nil, err 50 } 51 ··· 71 Value: block.RawData(), 72 } 73 74 + if err := bs.db.Create(ctx, &b, []clause.Expression{clause.OnConflict{ 75 Columns: []clause.Column{{Name: "did"}, {Name: "cid"}}, 76 UpdateAll: true, 77 }}).Error; err != nil { ··· 94 } 95 96 func (bs *SqliteBlockstore) PutMany(ctx context.Context, blocks []blocks.Block) error { 97 + tx := bs.db.BeginDangerously(ctx) 98 99 for _, block := range blocks { 100 bs.inserts[block.Cid()] = block
+1 -1
test.go
··· 32 33 u.Path = "xrpc/com.atproto.sync.subscribeRepos" 34 conn, _, err := dialer.Dial(u.String(), http.Header{ 35 - "User-Agent": []string{fmt.Sprintf("hot-topic/0.0.0")}, 36 }) 37 if err != nil { 38 return fmt.Errorf("subscribing to firehose failed (dialing): %w", err)
··· 32 33 u.Path = "xrpc/com.atproto.sync.subscribeRepos" 34 conn, _, err := dialer.Dial(u.String(), http.Header{ 35 + "User-Agent": []string{"cocoon-test/0.0.0"}, 36 }) 37 if err != nil { 38 return fmt.Errorf("subscribing to firehose failed (dialing): %w", err)