···1+name: Docker image
2+3+on:
4+ workflow_dispatch:
5+ push:
6+ branches:
7+ - main
8+ tags:
9+ - 'v*'
10+11+env:
12+ REGISTRY: ghcr.io
13+ IMAGE_NAME: ${{ github.repository }}
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
49+ uses: docker/metadata-action@v5
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+MIT License
2+3+Copyright (c) 2025 me@haileyok.com
4+5+Permission is hereby granted, free of charge, to any person obtaining a copy
6+of this software and associated documentation files (the "Software"), to deal
7+in the Software without restriction, including without limitation the rights
8+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+copies of the Software, and to permit persons to whom the Software is
10+furnished to do so, subject to the following conditions:
11+12+The above copyright notice and this permission notice shall be included in all
13+copies or substantial portions of the Software.
14+15+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+SOFTWARE.
+40
Makefile
···4GIT_COMMIT := $(shell git rev-parse --short=9 HEAD)
5VERSION := $(if $(GIT_TAG),$(GIT_TAG),dev-$(GIT_COMMIT))
6000000000000000007.PHONY: help
8help: ## Print info about all commands
9 @echo "Commands:"
···14build: ## Build all executables
15 go build -ldflags "-X main.Version=$(VERSION)" -o cocoon ./cmd/cocoon
16000000000000000000017.PHONY: run
18run:
19 go build -ldflags "-X main.Version=dev-local" -o cocoon ./cmd/cocoon && ./cocoon run
···4041.env:
42 if [ ! -f ".env" ]; then cp example.dev.env .env; fi
0000
···4GIT_COMMIT := $(shell git rev-parse --short=9 HEAD)
5VERSION := $(if $(GIT_TAG),$(GIT_TAG),dev-$(GIT_COMMIT))
67+# 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
25help: ## Print info about all commands
26 @echo "Commands:"
···31build: ## Build all executables
32 go build -ldflags "-X main.Version=$(VERSION)" -o cocoon ./cmd/cocoon
3334+.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
54run:
55 go build -ldflags "-X main.Version=dev-local" -o cocoon ./cmd/cocoon && ./cocoon run
···7677.env:
78 if [ ! -f ".env" ]; then cp example.dev.env .env; fi
79+80+.PHONY: docker-build
81+docker-build:
82+ docker build -t cocoon .
+253-60
README.md
···1# Cocoon
23> [!WARNING]
4-You should not use this PDS. You should not rely on this code as a reference for a PDS implementation. You should not trust this code. Using this PDS implementation may result in data loss, corruption, etc.
56Cocoon is a PDS implementation in Go. It is highly experimental, and is not ready for any production use.
78-### Impmlemented Endpoints
0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000910> [!NOTE]
11-Just because something is implemented doesn't mean it is finisehd. Tons of these are returning bad errors, don't do validation properly, etc. I'll make a "second pass" checklist at some point to do all of that.
1213-#### Identity
14-- [ ] com.atproto.identity.getRecommendedDidCredentials
15-- [ ] com.atproto.identity.requestPlcOperationSignature
16-- [x] com.atproto.identity.resolveHandle
17-- [ ] com.atproto.identity.signPlcOperation
18-- [ ] com.atproto.identity.submitPlcOperatioin
19-- [x] com.atproto.identity.updateHandle
2021-#### Repo
22-- [x] com.atproto.repo.applyWrites
23-- [x] com.atproto.repo.createRecord
24-- [x] com.atproto.repo.putRecord
25-- [x] com.atproto.repo.deleteRecord
26-- [x] com.atproto.repo.describeRepo
27-- [x] com.atproto.repo.getRecord
28-- [ ] com.atproto.repo.importRepo
29-- [x] com.atproto.repo.listRecords
30-- [ ] com.atproto.repo.listMissingBlobs
3132-#### Server
33-- [ ] com.atproto.server.activateAccount
34-- [ ] com.atproto.server.checkAccountStatus
35-- [x] com.atproto.server.confirmEmail
36-- [x] com.atproto.server.createAccount
37-- [x] com.atproto.server.createInviteCode
38-- [x] com.atproto.server.createInviteCodes
39-- [ ] com.atproto.server.deactivateAccount
40-- [ ] com.atproto.server.deleteAccount
41-- [x] com.atproto.server.deleteSession
42-- [x] com.atproto.server.describeServer
43-- [ ] com.atproto.server.getAccountInviteCodes
44-- [ ] com.atproto.server.getServiceAuth
45-- ~[ ] com.atproto.server.listAppPasswords~ - not going to add app passwords
46-- [x] com.atproto.server.refreshSession
47-- [ ] com.atproto.server.requestAccountDelete
48-- [x] com.atproto.server.requestEmailConfirmation
49-- [x] com.atproto.server.requestEmailUpdate
50-- [x] com.atproto.server.requestPasswordReset
51-- [ ] com.atproto.server.reserveSigningKey
52-- [x] com.atproto.server.resetPassword
53-- ~[ ] com.atproto.server.revokeAppPassword~ - not going to add app passwords
54-- [x] com.atproto.server.updateEmail
5556-#### Sync
57-- [x] com.atproto.sync.getBlob
58-- [x] com.atproto.sync.getBlocks
59-- [x] com.atproto.sync.getLatestCommit
60-- [x] com.atproto.sync.getRecord
61-- [x] com.atproto.sync.getRepoStatus
62-- [x] com.atproto.sync.getRepo
63-- [x] com.atproto.sync.listBlobs
64-- [x] com.atproto.sync.listRepos
65-- ~[ ] com.atproto.sync.notifyOfUpdate~ - BGS doesn't even have this implemented lol
66-- [x] com.atproto.sync.requestCrawl
67-- [x] com.atproto.sync.subscribeRepos
6869-#### Other
70-- [ ] com.atproto.label.queryLabels
71-- [ ] com.atproto.moderation.createReport
72-- [x] app.bsky.actor.getPreferences
73-- [x] app.bsky.actor.putPreferences
000000000000000000000000000000000000000000
···1# Cocoon
23> [!WARNING]
4+I migrated and have been running my main account on this PDS for months now without issue, however, I am still not responsible if things go awry, particularly during account migration. Please use caution.
56Cocoon is a PDS implementation in Go. It is highly experimental, and is not ready for any production use.
78+## Quick Start with Docker Compose
9+10+### Prerequisites
11+12+- Docker and Docker Compose installed
13+- A domain name pointing to your server (for automatic HTTPS)
14+- Ports 80 and 443 open in i.e. UFW
15+16+### Installation
17+18+1. **Clone the repository**
19+ ```bash
20+ git clone https://github.com/haileyok/cocoon.git
21+ cd cocoon
22+ ```
23+24+2. **Create your configuration file**
25+ ```bash
26+ cp .env.example .env
27+ ```
28+29+3. **Edit `.env` with your settings**
30+31+ Required settings:
32+ ```bash
33+ COCOON_DID="did:web:your-domain.com"
34+ COCOON_HOSTNAME="your-domain.com"
35+ COCOON_CONTACT_EMAIL="you@example.com"
36+ COCOON_RELAYS="https://bsky.network"
37+38+ # Generate with: openssl rand -hex 16
39+ COCOON_ADMIN_PASSWORD="your-secure-password"
40+41+ # Generate with: openssl rand -hex 32
42+ COCOON_SESSION_SECRET="your-session-secret"
43+ ```
44+45+4. **Start the services**
46+ ```bash
47+ # Pull pre-built image from GitHub Container Registry
48+ docker-compose pull
49+ docker-compose up -d
50+ ```
51+52+ Or build locally:
53+ ```bash
54+ docker-compose build
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:
67+ ```bash
68+ docker-compose logs create-invite
69+ ```
70+71+ Or check the saved file:
72+ ```bash
73+ cat keys/initial-invite-code.txt
74+ ```
75+76+ **IMPORTANT**: Save this invite code! You'll need it to create your first account.
77+78+6. **Monitor the services**
79+ ```bash
80+ docker-compose logs -f
81+ ```
82+83+### What Gets Set Up
84+85+The Docker Compose setup includes:
86+87+- **init-keys**: Automatically generates cryptographic keys (rotation key and JWK) on first run
88+- **cocoon**: The main PDS service running on port 8080
89+- **create-invite**: Automatically creates an initial invite code after Cocoon starts (first run only)
90+- **caddy**: Reverse proxy with automatic HTTPS via Let's Encrypt
91+92+### Data Persistence
93+94+The following directories will be created automatically:
95+96+- `./keys/` - Cryptographic keys (generated automatically)
97+ - `rotation.key` - PDS rotation key
98+ - `jwk.key` - JWK private key
99+ - `initial-invite-code.txt` - Your first invite code (first run only)
100+- `./data/` - SQLite database and blockstore
101+- Docker volumes for Caddy configuration and certificates
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"
132+COCOON_SMTP_PASS="your-smtp-password"
133+COCOON_SMTP_HOST="smtp.example.com"
134+COCOON_SMTP_PORT="587"
135+COCOON_SMTP_EMAIL="noreply@example.com"
136+COCOON_SMTP_NAME="Cocoon PDS"
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:
176+```bash
177+docker exec cocoon-pds /cocoon create-invite-code --uses 1
178+```
179+180+Reset a user's password:
181+```bash
182+docker exec cocoon-pds /cocoon reset-password --did "did:plc:xxx"
183+```
184+185+### Updating
186+187+```bash
188+docker-compose pull
189+docker-compose up -d
190+```
191+192+## Implemented Endpoints
193194> [!NOTE]
195+Just because something is implemented doesn't mean it is finished. Tons of these are returning bad errors, don't do validation properly, etc. I'll make a "second pass" checklist at some point to do all of that.
196197+### Identity
000000198199+- [x] `com.atproto.identity.getRecommendedDidCredentials`
200+- [x] `com.atproto.identity.requestPlcOperationSignature`
201+- [x] `com.atproto.identity.resolveHandle`
202+- [x] `com.atproto.identity.signPlcOperation`
203+- [x] `com.atproto.identity.submitPlcOperation`
204+- [x] `com.atproto.identity.updateHandle`
0000205206+### Repo
0000000000000000000000207208+- [x] `com.atproto.repo.applyWrites`
209+- [x] `com.atproto.repo.createRecord`
210+- [x] `com.atproto.repo.putRecord`
211+- [x] `com.atproto.repo.deleteRecord`
212+- [x] `com.atproto.repo.describeRepo`
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
0219220+- [x] `com.atproto.server.activateAccount`
221+- [x] `com.atproto.server.checkAccountStatus`
222+- [x] `com.atproto.server.confirmEmail`
223+- [x] `com.atproto.server.createAccount`
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`
242+243+### Sync
244+245+- [x] `com.atproto.sync.getBlob`
246+- [x] `com.atproto.sync.getBlocks`
247+- [x] `com.atproto.sync.getLatestCommit`
248+- [x] `com.atproto.sync.getRecord`
249+- [x] `com.atproto.sync.getRepoStatus`
250+- [x] `com.atproto.sync.getRepo`
251+- [x] `com.atproto.sync.listBlobs`
252+- [x] `com.atproto.sync.listRepos`
253+- ~~[ ] `com.atproto.sync.notifyOfUpdate`~~ - BGS doesn't even have this implemented lol
254+- [x] `com.atproto.sync.requestCrawl`
255+- [x] `com.atproto.sync.subscribeRepos`
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`
263+264+## License
265+266+This project is licensed under MIT license. `server/static/pico.css` is also licensed under MIT license, available at [https://github.com/picocss/pico/](https://github.com/picocss/pico/).
···1package helpers
23import (
0004 "math/rand"
0506 "github.com/labstack/echo/v4"
07)
89// This will confirm to the regex in the application if 5 chars are used for each side of the -
···26 return genericError(e, 400, msg)
27}
28000000000000000000000000000029func genericError(e echo.Context, code int, msg string) error {
30 return e.JSON(code, map[string]string{
31 "error": msg,
···39 }
40 return string(b)
41}
000000000000000000000000000000000000000000
···10// This is kinda lame. Not great to implement app.bsky in the pds, but alas
1112func (s *Server) handleActorPutPreferences(e echo.Context) error {
0013 repo := e.Get("repo").(*models.RepoActor)
1415 var prefs map[string]any
···22 return err
23 }
2425- if err := s.db.Exec("UPDATE repos SET preferences = ? WHERE did = ?", 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
1112func (s *Server) handleActorPutPreferences(e echo.Context) error {
13+ ctx := e.Request().Context()
14+15 repo := e.Get("repo").(*models.RepoActor)
1617 var prefs map[string]any
···24 return err
25 }
2627+ 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