An atproto PDS written in Go

Compare changes

Choose any two refs to compare.

Changed files
+1627 -381
.github
workflows
cmd
cocoon
identity
internal
db
helpers
models
oauth
plc
recording_blockstore
server
sqlite_blockstore
+1 -1
.env.example
···
COCOON_RELAYS=https://bsky.network
# Generate with `openssl rand -hex 16`
COCOON_ADMIN_PASSWORD=
-
# openssl rand -hex 32
COCOON_SESSION_SECRET=
···
COCOON_RELAYS=https://bsky.network
# Generate with `openssl rand -hex 16`
COCOON_ADMIN_PASSWORD=
+
# Generate with `openssl rand -hex 32`
COCOON_SESSION_SECRET=
+60
.github/workflows/docker-image.yml
···
···
+
name: Docker image
+
+
on:
+
workflow_dispatch:
+
push:
+
branches:
+
- main
+
+
env:
+
REGISTRY: ghcr.io
+
IMAGE_NAME: ${{ github.repository }}
+
+
jobs:
+
build-and-push-image:
+
runs-on: ubuntu-latest
+
# Sets the permissions granted to the `GITHUB_TOKEN` for the actions in this job.
+
permissions:
+
contents: read
+
packages: write
+
attestations: write
+
id-token: write
+
#
+
steps:
+
- name: Checkout repository
+
uses: actions/checkout@v4
+
# 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.
+
- name: Log in to the Container registry
+
uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1
+
with:
+
registry: ${{ env.REGISTRY }}
+
username: ${{ github.actor }}
+
password: ${{ secrets.GITHUB_TOKEN }}
+
# 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.
+
- name: Extract metadata (tags, labels) for Docker
+
id: meta
+
uses: docker/metadata-action@v5
+
with:
+
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
+
tags: |
+
type=sha
+
type=sha,format=long
+
# 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.
+
# 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.
+
# It uses the `tags` and `labels` parameters to tag and label the image with the output from the "meta" step.
+
- name: Build and push Docker image
+
id: push
+
uses: docker/build-push-action@v5
+
with:
+
context: .
+
push: true
+
tags: ${{ steps.meta.outputs.tags }}
+
labels: ${{ steps.meta.outputs.labels }}
+
+
# 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)."
+
- name: Generate artifact attestation
+
uses: actions/attest-build-provenance@v1
+
with:
+
subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME}}
+
subject-digest: ${{ steps.push.outputs.digest }}
+
push-to-registry: true
+2
.gitignore
···
*.key
*.secret
.DS_Store
···
*.key
*.secret
.DS_Store
+
data/
+
keys/
+10
Caddyfile
···
···
+
{$COCOON_HOSTNAME} {
+
reverse_proxy localhost:8080
+
+
encode gzip
+
+
log {
+
output file /data/access.log
+
format json
+
}
+
}
+25
Dockerfile
···
···
+
### Compile stage
+
FROM golang:1.25.1-bookworm AS build-env
+
+
ADD . /dockerbuild
+
WORKDIR /dockerbuild
+
+
RUN GIT_VERSION=$(git describe --tags --long --always || echo "dev-local") && \
+
go mod tidy && \
+
go build -ldflags "-X main.Version=$GIT_VERSION" -o cocoon ./cmd/cocoon
+
+
### Run stage
+
FROM debian:bookworm-slim AS run
+
+
RUN apt-get update && apt-get install -y dumb-init runit ca-certificates && rm -rf /var/lib/apt/lists/*
+
ENTRYPOINT ["dumb-init", "--"]
+
+
WORKDIR /
+
RUN mkdir -p data/cocoon
+
COPY --from=build-env /dockerbuild/cocoon /
+
+
CMD ["/cocoon", "run"]
+
+
LABEL org.opencontainers.image.source=https://github.com/haileyok/cocoon
+
LABEL org.opencontainers.image.description="Cocoon ATProto PDS"
+
LABEL org.opencontainers.image.licenses=MIT
+4
Makefile
···
.env:
if [ ! -f ".env" ]; then cp example.dev.env .env; fi
···
.env:
if [ ! -f ".env" ]; then cp example.dev.env .env; fi
+
+
.PHONY: docker-build
+
docker-build:
+
docker build -t cocoon .
+139 -8
README.md
···
# Cocoon
> [!WARNING]
-
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.
Cocoon is a PDS implementation in Go. It is highly experimental, and is not ready for any production use.
## Implemented Endpoints
> [!NOTE]
···
### Identity
-
- [ ] `com.atproto.identity.getRecommendedDidCredentials`
-
- [ ] `com.atproto.identity.requestPlcOperationSignature`
- [x] `com.atproto.identity.resolveHandle`
-
- [ ] `com.atproto.identity.signPlcOperation`
-
- [ ] `com.atproto.identity.submitPlcOperation`
- [x] `com.atproto.identity.updateHandle`
### Repo
···
- [x] `com.atproto.repo.deleteRecord`
- [x] `com.atproto.repo.describeRepo`
- [x] `com.atproto.repo.getRecord`
-
- [x] `com.atproto.repo.importRepo` (Works "okay". You still have to handle PLC operations on your own when migrating. Use with extreme caution.)
- [x] `com.atproto.repo.listRecords`
- [ ] `com.atproto.repo.listMissingBlobs`
### Server
-
- [ ] `com.atproto.server.activateAccount`
- [x] `com.atproto.server.checkAccountStatus`
- [x] `com.atproto.server.confirmEmail`
- [x] `com.atproto.server.createAccount`
- [x] `com.atproto.server.createInviteCode`
- [x] `com.atproto.server.createInviteCodes`
-
- [ ] `com.atproto.server.deactivateAccount`
- [ ] `com.atproto.server.deleteAccount`
- [x] `com.atproto.server.deleteSession`
- [x] `com.atproto.server.describeServer`
···
# Cocoon
> [!WARNING]
+
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.
Cocoon is a PDS implementation in Go. It is highly experimental, and is not ready for any production use.
+
## Quick Start with Docker Compose
+
+
### Prerequisites
+
+
- Docker and Docker Compose installed
+
- A domain name pointing to your server (for automatic HTTPS)
+
- Ports 80 and 443 open in i.e. UFW
+
+
### Installation
+
+
1. **Clone the repository**
+
```bash
+
git clone https://github.com/haileyok/cocoon.git
+
cd cocoon
+
```
+
+
2. **Create your configuration file**
+
```bash
+
cp .env.example .env
+
```
+
+
3. **Edit `.env` with your settings**
+
+
Required settings:
+
```bash
+
COCOON_DID="did:web:your-domain.com"
+
COCOON_HOSTNAME="your-domain.com"
+
COCOON_CONTACT_EMAIL="you@example.com"
+
COCOON_RELAYS="https://bsky.network"
+
+
# Generate with: openssl rand -hex 16
+
COCOON_ADMIN_PASSWORD="your-secure-password"
+
+
# Generate with: openssl rand -hex 32
+
COCOON_SESSION_SECRET="your-session-secret"
+
```
+
+
4. **Start the services**
+
```bash
+
# Pull pre-built image from GitHub Container Registry
+
docker-compose pull
+
docker-compose up -d
+
```
+
+
Or build locally:
+
```bash
+
docker-compose build
+
docker-compose up -d
+
```
+
+
5. **Get your invite code**
+
+
On first run, an invite code is automatically created. View it with:
+
```bash
+
docker-compose logs create-invite
+
```
+
+
Or check the saved file:
+
```bash
+
cat keys/initial-invite-code.txt
+
```
+
+
**IMPORTANT**: Save this invite code! You'll need it to create your first account.
+
+
6. **Monitor the services**
+
```bash
+
docker-compose logs -f
+
```
+
+
### What Gets Set Up
+
+
The Docker Compose setup includes:
+
+
- **init-keys**: Automatically generates cryptographic keys (rotation key and JWK) on first run
+
- **cocoon**: The main PDS service running on port 8080
+
- **create-invite**: Automatically creates an initial invite code after Cocoon starts (first run only)
+
- **caddy**: Reverse proxy with automatic HTTPS via Let's Encrypt
+
+
### Data Persistence
+
+
The following directories will be created automatically:
+
+
- `./keys/` - Cryptographic keys (generated automatically)
+
- `rotation.key` - PDS rotation key
+
- `jwk.key` - JWK private key
+
- `initial-invite-code.txt` - Your first invite code (first run only)
+
- `./data/` - SQLite database and blockstore
+
- Docker volumes for Caddy configuration and certificates
+
+
### Optional Configuration
+
+
#### SMTP Email Settings
+
```bash
+
COCOON_SMTP_USER="your-smtp-username"
+
COCOON_SMTP_PASS="your-smtp-password"
+
COCOON_SMTP_HOST="smtp.example.com"
+
COCOON_SMTP_PORT="587"
+
COCOON_SMTP_EMAIL="noreply@example.com"
+
COCOON_SMTP_NAME="Cocoon PDS"
+
```
+
+
#### S3 Storage
+
```bash
+
COCOON_S3_BACKUPS_ENABLED=true
+
COCOON_S3_BLOBSTORE_ENABLED=true
+
COCOON_S3_REGION="us-east-1"
+
COCOON_S3_BUCKET="your-bucket"
+
COCOON_S3_ENDPOINT="https://s3.amazonaws.com"
+
COCOON_S3_ACCESS_KEY="your-access-key"
+
COCOON_S3_SECRET_KEY="your-secret-key"
+
```
+
+
### Management Commands
+
+
Create an invite code:
+
```bash
+
docker exec cocoon-pds /cocoon create-invite-code --uses 1
+
```
+
+
Reset a user's password:
+
```bash
+
docker exec cocoon-pds /cocoon reset-password --did "did:plc:xxx"
+
```
+
+
### Updating
+
+
```bash
+
docker-compose pull
+
docker-compose up -d
+
```
+
## Implemented Endpoints
> [!NOTE]
···
### Identity
+
- [x] `com.atproto.identity.getRecommendedDidCredentials`
+
- [x] `com.atproto.identity.requestPlcOperationSignature`
- [x] `com.atproto.identity.resolveHandle`
+
- [x] `com.atproto.identity.signPlcOperation`
+
- [x] `com.atproto.identity.submitPlcOperation`
- [x] `com.atproto.identity.updateHandle`
### Repo
···
- [x] `com.atproto.repo.deleteRecord`
- [x] `com.atproto.repo.describeRepo`
- [x] `com.atproto.repo.getRecord`
+
- [x] `com.atproto.repo.importRepo` (Works "okay". Use with extreme caution.)
- [x] `com.atproto.repo.listRecords`
- [ ] `com.atproto.repo.listMissingBlobs`
### Server
+
- [x] `com.atproto.server.activateAccount`
- [x] `com.atproto.server.checkAccountStatus`
- [x] `com.atproto.server.confirmEmail`
- [x] `com.atproto.server.createAccount`
- [x] `com.atproto.server.createInviteCode`
- [x] `com.atproto.server.createInviteCodes`
+
- [x] `com.atproto.server.deactivateAccount`
- [ ] `com.atproto.server.deleteAccount`
- [x] `com.atproto.server.deleteSession`
- [x] `com.atproto.server.describeServer`
+46 -55
cmd/cocoon/main.go
···
"os"
"time"
-
"github.com/bluesky-social/indigo/atproto/crypto"
"github.com/bluesky-social/indigo/atproto/syntax"
"github.com/haileyok/cocoon/internal/helpers"
"github.com/haileyok/cocoon/server"
···
EnvVars: []string{"COCOON_DB_NAME"},
},
&cli.StringFlag{
-
Name: "did",
-
Required: true,
-
EnvVars: []string{"COCOON_DID"},
},
&cli.StringFlag{
-
Name: "hostname",
-
Required: true,
-
EnvVars: []string{"COCOON_HOSTNAME"},
},
&cli.StringFlag{
-
Name: "rotation-key-path",
-
Required: true,
-
EnvVars: []string{"COCOON_ROTATION_KEY_PATH"},
},
&cli.StringFlag{
-
Name: "jwk-path",
-
Required: true,
-
EnvVars: []string{"COCOON_JWK_PATH"},
},
&cli.StringFlag{
-
Name: "contact-email",
-
Required: true,
-
EnvVars: []string{"COCOON_CONTACT_EMAIL"},
},
&cli.StringSliceFlag{
-
Name: "relays",
-
Required: true,
-
EnvVars: []string{"COCOON_RELAYS"},
},
&cli.StringFlag{
-
Name: "admin-password",
-
Required: true,
-
EnvVars: []string{"COCOON_ADMIN_PASSWORD"},
},
&cli.StringFlag{
-
Name: "smtp-user",
-
Required: false,
-
EnvVars: []string{"COCOON_SMTP_USER"},
},
&cli.StringFlag{
-
Name: "smtp-pass",
-
Required: false,
-
EnvVars: []string{"COCOON_SMTP_PASS"},
},
&cli.StringFlag{
-
Name: "smtp-host",
-
Required: false,
-
EnvVars: []string{"COCOON_SMTP_HOST"},
},
&cli.StringFlag{
-
Name: "smtp-port",
-
Required: false,
-
EnvVars: []string{"COCOON_SMTP_PORT"},
},
&cli.StringFlag{
-
Name: "smtp-email",
-
Required: false,
-
EnvVars: []string{"COCOON_SMTP_EMAIL"},
},
&cli.StringFlag{
-
Name: "smtp-name",
-
Required: false,
-
EnvVars: []string{"COCOON_SMTP_NAME"},
},
&cli.BoolFlag{
Name: "s3-backups-enabled",
EnvVars: []string{"COCOON_S3_BACKUPS_ENABLED"},
},
&cli.StringFlag{
Name: "s3-region",
···
&cli.StringFlag{
Name: "session-secret",
EnvVars: []string{"COCOON_SESSION_SECRET"},
-
},
-
&cli.StringFlag{
-
Name: "default-atproto-proxy",
-
EnvVars: []string{"COCOON_DEFAULT_ATPROTO_PROXY"},
-
Value: "did:web:api.bsky.app#bsky_appview",
},
&cli.StringFlag{
Name: "blockstore-variant",
EnvVars: []string{"COCOON_BLOCKSTORE_VARIANT"},
Value: "sqlite",
},
},
Commands: []*cli.Command{
runServe,
···
SmtpEmail: cmd.String("smtp-email"),
SmtpName: cmd.String("smtp-name"),
S3Config: &server.S3Config{
-
BackupsEnabled: cmd.Bool("s3-backups-enabled"),
-
Region: cmd.String("s3-region"),
-
Bucket: cmd.String("s3-bucket"),
-
Endpoint: cmd.String("s3-endpoint"),
-
AccessKey: cmd.String("s3-access-key"),
-
SecretKey: cmd.String("s3-secret-key"),
},
-
SessionSecret: cmd.String("session-secret"),
-
DefaultAtprotoProxy: cmd.String("default-atproto-proxy"),
-
BlockstoreVariant: server.MustReturnBlockstoreVariant(cmd.String("blockstore-variant")),
})
if err != nil {
fmt.Printf("error creating cocoon: %v", err)
···
},
},
Action: func(cmd *cli.Context) error {
-
key, err := crypto.GeneratePrivateKeyK256()
if err != nil {
return err
}
···
"os"
"time"
+
"github.com/bluesky-social/indigo/atproto/atcrypto"
"github.com/bluesky-social/indigo/atproto/syntax"
"github.com/haileyok/cocoon/internal/helpers"
"github.com/haileyok/cocoon/server"
···
EnvVars: []string{"COCOON_DB_NAME"},
},
&cli.StringFlag{
+
Name: "did",
+
EnvVars: []string{"COCOON_DID"},
},
&cli.StringFlag{
+
Name: "hostname",
+
EnvVars: []string{"COCOON_HOSTNAME"},
},
&cli.StringFlag{
+
Name: "rotation-key-path",
+
EnvVars: []string{"COCOON_ROTATION_KEY_PATH"},
},
&cli.StringFlag{
+
Name: "jwk-path",
+
EnvVars: []string{"COCOON_JWK_PATH"},
},
&cli.StringFlag{
+
Name: "contact-email",
+
EnvVars: []string{"COCOON_CONTACT_EMAIL"},
},
&cli.StringSliceFlag{
+
Name: "relays",
+
EnvVars: []string{"COCOON_RELAYS"},
},
&cli.StringFlag{
+
Name: "admin-password",
+
EnvVars: []string{"COCOON_ADMIN_PASSWORD"},
},
&cli.StringFlag{
+
Name: "smtp-user",
+
EnvVars: []string{"COCOON_SMTP_USER"},
},
&cli.StringFlag{
+
Name: "smtp-pass",
+
EnvVars: []string{"COCOON_SMTP_PASS"},
},
&cli.StringFlag{
+
Name: "smtp-host",
+
EnvVars: []string{"COCOON_SMTP_HOST"},
},
&cli.StringFlag{
+
Name: "smtp-port",
+
EnvVars: []string{"COCOON_SMTP_PORT"},
},
&cli.StringFlag{
+
Name: "smtp-email",
+
EnvVars: []string{"COCOON_SMTP_EMAIL"},
},
&cli.StringFlag{
+
Name: "smtp-name",
+
EnvVars: []string{"COCOON_SMTP_NAME"},
},
&cli.BoolFlag{
Name: "s3-backups-enabled",
EnvVars: []string{"COCOON_S3_BACKUPS_ENABLED"},
+
},
+
&cli.BoolFlag{
+
Name: "s3-blobstore-enabled",
+
EnvVars: []string{"COCOON_S3_BLOBSTORE_ENABLED"},
},
&cli.StringFlag{
Name: "s3-region",
···
&cli.StringFlag{
Name: "session-secret",
EnvVars: []string{"COCOON_SESSION_SECRET"},
},
&cli.StringFlag{
Name: "blockstore-variant",
EnvVars: []string{"COCOON_BLOCKSTORE_VARIANT"},
Value: "sqlite",
},
+
&cli.StringFlag{
+
Name: "fallback-proxy",
+
EnvVars: []string{"COCOON_FALLBACK_PROXY"},
+
},
},
Commands: []*cli.Command{
runServe,
···
SmtpEmail: cmd.String("smtp-email"),
SmtpName: cmd.String("smtp-name"),
S3Config: &server.S3Config{
+
BackupsEnabled: cmd.Bool("s3-backups-enabled"),
+
BlobstoreEnabled: cmd.Bool("s3-blobstore-enabled"),
+
Region: cmd.String("s3-region"),
+
Bucket: cmd.String("s3-bucket"),
+
Endpoint: cmd.String("s3-endpoint"),
+
AccessKey: cmd.String("s3-access-key"),
+
SecretKey: cmd.String("s3-secret-key"),
},
+
SessionSecret: cmd.String("session-secret"),
+
BlockstoreVariant: server.MustReturnBlockstoreVariant(cmd.String("blockstore-variant")),
+
FallbackProxy: cmd.String("fallback-proxy"),
})
if err != nil {
fmt.Printf("error creating cocoon: %v", err)
···
},
},
Action: func(cmd *cli.Context) error {
+
key, err := atcrypto.GeneratePrivateKeyK256()
if err != nil {
return err
}
+56
create-initial-invite.sh
···
···
+
#!/bin/sh
+
+
INVITE_FILE="/keys/initial-invite-code.txt"
+
MARKER="/keys/.invite_created"
+
+
# Check if invite code was already created
+
if [ -f "$MARKER" ]; then
+
echo "โœ“ Initial invite code already created"
+
exit 0
+
fi
+
+
echo "Waiting for database to be ready..."
+
sleep 10
+
+
# Try to create invite code - retry until database is ready
+
MAX_ATTEMPTS=30
+
ATTEMPT=0
+
INVITE_CODE=""
+
+
while [ $ATTEMPT -lt $MAX_ATTEMPTS ]; do
+
ATTEMPT=$((ATTEMPT + 1))
+
OUTPUT=$(/cocoon create-invite-code --uses 1 2>&1)
+
INVITE_CODE=$(echo "$OUTPUT" | grep -oE '[a-zA-Z0-9]{8}-[a-zA-Z0-9]{8}' || echo "")
+
+
if [ -n "$INVITE_CODE" ]; then
+
break
+
fi
+
+
if [ $((ATTEMPT % 5)) -eq 0 ]; then
+
echo " Waiting for database... ($ATTEMPT/$MAX_ATTEMPTS)"
+
fi
+
sleep 2
+
done
+
+
if [ -n "$INVITE_CODE" ]; then
+
echo ""
+
echo "โ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•—"
+
echo "โ•‘ SAVE THIS INVITE CODE! โ•‘"
+
echo "โ•‘ โ•‘"
+
echo "โ•‘ $INVITE_CODE โ•‘"
+
echo "โ•‘ โ•‘"
+
echo "โ•‘ Use this to create your first โ•‘"
+
echo "โ•‘ account on your PDS. โ•‘"
+
echo "โ•šโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•"
+
echo ""
+
+
echo "$INVITE_CODE" > "$INVITE_FILE"
+
echo "โœ“ Invite code saved to: $INVITE_FILE"
+
+
touch "$MARKER"
+
echo "โœ“ Initial setup complete!"
+
else
+
echo "โœ— Failed to create invite code"
+
echo "Output: $OUTPUT"
+
exit 1
+
fi
+125
docker-compose.yaml
···
···
+
version: '3.8'
+
+
services:
+
init-keys:
+
build:
+
context: .
+
dockerfile: Dockerfile
+
image: ghcr.io/haileyok/cocoon:latest
+
container_name: cocoon-init-keys
+
volumes:
+
- ./keys:/keys
+
- ./data:/data/cocoon
+
- ./init-keys.sh:/init-keys.sh:ro
+
environment:
+
COCOON_DID: ${COCOON_DID}
+
COCOON_HOSTNAME: ${COCOON_HOSTNAME}
+
COCOON_ROTATION_KEY_PATH: /keys/rotation.key
+
COCOON_JWK_PATH: /keys/jwk.key
+
COCOON_CONTACT_EMAIL: ${COCOON_CONTACT_EMAIL}
+
COCOON_RELAYS: ${COCOON_RELAYS:-https://bsky.network}
+
COCOON_ADMIN_PASSWORD: ${COCOON_ADMIN_PASSWORD}
+
entrypoint: ["/bin/sh", "/init-keys.sh"]
+
restart: "no"
+
+
cocoon:
+
build:
+
context: .
+
dockerfile: Dockerfile
+
image: ghcr.io/haileyok/cocoon:latest
+
container_name: cocoon-pds
+
network_mode: host
+
depends_on:
+
init-keys:
+
condition: service_completed_successfully
+
volumes:
+
- ./data:/data/cocoon
+
- ./keys/rotation.key:/keys/rotation.key:ro
+
- ./keys/jwk.key:/keys/jwk.key:ro
+
environment:
+
# Required settings
+
COCOON_DID: ${COCOON_DID}
+
COCOON_HOSTNAME: ${COCOON_HOSTNAME}
+
COCOON_ROTATION_KEY_PATH: /keys/rotation.key
+
COCOON_JWK_PATH: /keys/jwk.key
+
COCOON_CONTACT_EMAIL: ${COCOON_CONTACT_EMAIL}
+
COCOON_RELAYS: ${COCOON_RELAYS:-https://bsky.network}
+
COCOON_ADMIN_PASSWORD: ${COCOON_ADMIN_PASSWORD}
+
COCOON_SESSION_SECRET: ${COCOON_SESSION_SECRET}
+
+
# Server configuration
+
COCOON_ADDR: ":8080"
+
COCOON_DB_NAME: /data/cocoon/cocoon.db
+
COCOON_BLOCKSTORE_VARIANT: ${COCOON_BLOCKSTORE_VARIANT:-sqlite}
+
+
# Optional: SMTP settings for email
+
COCOON_SMTP_USER: ${COCOON_SMTP_USER:-}
+
COCOON_SMTP_PASS: ${COCOON_SMTP_PASS:-}
+
COCOON_SMTP_HOST: ${COCOON_SMTP_HOST:-}
+
COCOON_SMTP_PORT: ${COCOON_SMTP_PORT:-}
+
COCOON_SMTP_EMAIL: ${COCOON_SMTP_EMAIL:-}
+
COCOON_SMTP_NAME: ${COCOON_SMTP_NAME:-}
+
+
# Optional: S3 configuration
+
COCOON_S3_BACKUPS_ENABLED: ${COCOON_S3_BACKUPS_ENABLED:-false}
+
COCOON_S3_BLOBSTORE_ENABLED: ${COCOON_S3_BLOBSTORE_ENABLED:-false}
+
COCOON_S3_REGION: ${COCOON_S3_REGION:-}
+
COCOON_S3_BUCKET: ${COCOON_S3_BUCKET:-}
+
COCOON_S3_ENDPOINT: ${COCOON_S3_ENDPOINT:-}
+
COCOON_S3_ACCESS_KEY: ${COCOON_S3_ACCESS_KEY:-}
+
COCOON_S3_SECRET_KEY: ${COCOON_S3_SECRET_KEY:-}
+
+
# Optional: Fallback proxy
+
COCOON_FALLBACK_PROXY: ${COCOON_FALLBACK_PROXY:-}
+
restart: unless-stopped
+
healthcheck:
+
test: ["CMD", "curl", "-f", "http://localhost:8080/xrpc/_health"]
+
interval: 30s
+
timeout: 10s
+
retries: 3
+
start_period: 40s
+
+
create-invite:
+
build:
+
context: .
+
dockerfile: Dockerfile
+
image: ghcr.io/haileyok/cocoon:latest
+
container_name: cocoon-create-invite
+
network_mode: host
+
volumes:
+
- ./keys:/keys
+
- ./create-initial-invite.sh:/create-initial-invite.sh:ro
+
environment:
+
COCOON_DID: ${COCOON_DID}
+
COCOON_HOSTNAME: ${COCOON_HOSTNAME}
+
COCOON_ROTATION_KEY_PATH: /keys/rotation.key
+
COCOON_JWK_PATH: /keys/jwk.key
+
COCOON_CONTACT_EMAIL: ${COCOON_CONTACT_EMAIL}
+
COCOON_RELAYS: ${COCOON_RELAYS:-https://bsky.network}
+
COCOON_ADMIN_PASSWORD: ${COCOON_ADMIN_PASSWORD}
+
COCOON_DB_NAME: /data/cocoon/cocoon.db
+
depends_on:
+
- init-keys
+
entrypoint: ["/bin/sh", "/create-initial-invite.sh"]
+
restart: "no"
+
+
caddy:
+
image: caddy:2-alpine
+
container_name: cocoon-caddy
+
network_mode: host
+
volumes:
+
- ./Caddyfile:/etc/caddy/Caddyfile:ro
+
- caddy_data:/data
+
- caddy_config:/config
+
restart: unless-stopped
+
environment:
+
COCOON_HOSTNAME: ${COCOON_HOSTNAME}
+
CADDY_ACME_EMAIL: ${COCOON_CONTACT_EMAIL:-}
+
+
volumes:
+
data:
+
driver: local
+
caddy_data:
+
driver: local
+
caddy_config:
+
driver: local
+2 -2
go.mod
···
require (
github.com/Azure/go-autorest/autorest/to v0.4.1
github.com/aws/aws-sdk-go v1.55.7
-
github.com/bluesky-social/indigo v0.0.0-20250414202759-826fcdeaa36b
github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792
github.com/domodwyer/mailyak/v3 v3.6.2
github.com/go-pkgz/expirable-cache/v3 v3.0.0
···
github.com/hashicorp/golang-lru/v2 v2.0.7
github.com/ipfs/go-block-format v0.2.0
github.com/ipfs/go-cid v0.4.1
github.com/ipfs/go-ipld-cbor v0.1.0
github.com/ipld/go-car v0.6.1-0.20230509095817-92d28eb23ba4
github.com/joho/godotenv v1.5.1
···
github.com/ipfs/bbloom v0.0.4 // indirect
github.com/ipfs/go-blockservice v0.5.2 // indirect
github.com/ipfs/go-datastore v0.6.0 // indirect
-
github.com/ipfs/go-ipfs-blockstore v1.3.1 // indirect
github.com/ipfs/go-ipfs-ds-help v1.1.1 // indirect
github.com/ipfs/go-ipfs-exchange-interface v0.2.1 // indirect
github.com/ipfs/go-ipfs-util v0.0.3 // indirect
···
require (
github.com/Azure/go-autorest/autorest/to v0.4.1
github.com/aws/aws-sdk-go v1.55.7
+
github.com/bluesky-social/indigo v0.0.0-20251009212240-20524de167fe
github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792
github.com/domodwyer/mailyak/v3 v3.6.2
github.com/go-pkgz/expirable-cache/v3 v3.0.0
···
github.com/hashicorp/golang-lru/v2 v2.0.7
github.com/ipfs/go-block-format v0.2.0
github.com/ipfs/go-cid v0.4.1
+
github.com/ipfs/go-ipfs-blockstore v1.3.1
github.com/ipfs/go-ipld-cbor v0.1.0
github.com/ipld/go-car v0.6.1-0.20230509095817-92d28eb23ba4
github.com/joho/godotenv v1.5.1
···
github.com/ipfs/bbloom v0.0.4 // indirect
github.com/ipfs/go-blockservice v0.5.2 // indirect
github.com/ipfs/go-datastore v0.6.0 // indirect
github.com/ipfs/go-ipfs-ds-help v1.1.1 // indirect
github.com/ipfs/go-ipfs-exchange-interface v0.2.1 // indirect
github.com/ipfs/go-ipfs-util v0.0.3 // indirect
+2 -4
go.sum
···
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932 h1:mXoPYz/Ul5HYEDvkta6I8/rnYM5gSdSV2tJ6XbZuEtY=
github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932/go.mod h1:NOuUCSz6Q9T7+igc/hlvDOUdtWKryOrtFyIVABv/p7k=
-
github.com/bluesky-social/indigo v0.0.0-20250414202759-826fcdeaa36b h1:elwfbe+W7GkUmPKFX1h7HaeHvC/kC0XJWfiEHC62xPg=
-
github.com/bluesky-social/indigo v0.0.0-20250414202759-826fcdeaa36b/go.mod h1:yjdhLA1LkK8VDS/WPUoYPo25/Hq/8rX38Ftr67EsqKY=
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY=
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792 h1:R8vQdOQdZ9Y3SkEwmHoWBmX1DNXhXZqlTpq6s4tyJGc=
···
github.com/ipfs/go-block-format v0.2.0/go.mod h1:+jpL11nFx5A/SPpsoBn6Bzkra/zaArfSmsknbPMYgzM=
github.com/ipfs/go-blockservice v0.5.2 h1:in9Bc+QcXwd1apOVM7Un9t8tixPKdaHQFdLSUM1Xgk8=
github.com/ipfs/go-blockservice v0.5.2/go.mod h1:VpMblFEqG67A/H2sHKAemeH9vlURVavlysbdUI632yk=
-
github.com/ipfs/go-bs-sqlite3 v0.0.0-20221122195556-bfcee1be620d h1:9V+GGXCuOfDiFpdAHz58q9mKLg447xp0cQKvqQrAwYE=
-
github.com/ipfs/go-bs-sqlite3 v0.0.0-20221122195556-bfcee1be620d/go.mod h1:pMbnFyNAGjryYCLCe59YDLRv/ujdN+zGJBT1umlvYRM=
github.com/ipfs/go-cid v0.4.1 h1:A/T3qGvxi4kpKWWcPC/PgbvDA2bjVLO7n4UeVwnbs/s=
github.com/ipfs/go-cid v0.4.1/go.mod h1:uQHwDeX4c6CtyrFwdqyhpNcxVewur1M7l7fNU7LKwZk=
github.com/ipfs/go-datastore v0.6.0 h1:JKyz+Gvz1QEZw0LsX1IBn+JFCJQH4SJVFtM4uWU0Myk=
···
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932 h1:mXoPYz/Ul5HYEDvkta6I8/rnYM5gSdSV2tJ6XbZuEtY=
github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932/go.mod h1:NOuUCSz6Q9T7+igc/hlvDOUdtWKryOrtFyIVABv/p7k=
+
github.com/bluesky-social/indigo v0.0.0-20251009212240-20524de167fe h1:VBhaqE5ewQgXbY5SfSWFZC/AwHFo7cHxZKFYi2ce9Yo=
+
github.com/bluesky-social/indigo v0.0.0-20251009212240-20524de167fe/go.mod h1:RuQVrCGm42QNsgumKaR6se+XkFKfCPNwdCiTvqKRUck=
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY=
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792 h1:R8vQdOQdZ9Y3SkEwmHoWBmX1DNXhXZqlTpq6s4tyJGc=
···
github.com/ipfs/go-block-format v0.2.0/go.mod h1:+jpL11nFx5A/SPpsoBn6Bzkra/zaArfSmsknbPMYgzM=
github.com/ipfs/go-blockservice v0.5.2 h1:in9Bc+QcXwd1apOVM7Un9t8tixPKdaHQFdLSUM1Xgk8=
github.com/ipfs/go-blockservice v0.5.2/go.mod h1:VpMblFEqG67A/H2sHKAemeH9vlURVavlysbdUI632yk=
github.com/ipfs/go-cid v0.4.1 h1:A/T3qGvxi4kpKWWcPC/PgbvDA2bjVLO7n4UeVwnbs/s=
github.com/ipfs/go-cid v0.4.1/go.mod h1:uQHwDeX4c6CtyrFwdqyhpNcxVewur1M7l7fNU7LKwZk=
github.com/ipfs/go-datastore v0.6.0 h1:JKyz+Gvz1QEZw0LsX1IBn+JFCJQH4SJVFtM4uWU0Myk=
+3 -3
identity/identity.go
···
}
func ResolveHandleFromWellKnown(ctx context.Context, cli *http.Client, handle string) (string, error) {
-
ustr := fmt.Sprintf("https://%s/.well=known/atproto-did", handle)
req, err := http.NewRequestWithContext(
ctx,
"GET",
···
func DidToDocUrl(did string) (string, error) {
if strings.HasPrefix(did, "did:plc:") {
return fmt.Sprintf("https://plc.directory/%s", did), nil
-
} else if strings.HasPrefix(did, "did:web:") {
-
return fmt.Sprintf("https://%s/.well-known/did.json", strings.TrimPrefix(did, "did:web:")), nil
} else {
return "", fmt.Errorf("did was not a supported did type")
}
···
}
func ResolveHandleFromWellKnown(ctx context.Context, cli *http.Client, handle string) (string, error) {
+
ustr := fmt.Sprintf("https://%s/.well-known/atproto-did", handle)
req, err := http.NewRequestWithContext(
ctx,
"GET",
···
func DidToDocUrl(did string) (string, error) {
if strings.HasPrefix(did, "did:plc:") {
return fmt.Sprintf("https://plc.directory/%s", did), nil
+
} else if after, ok := strings.CutPrefix(did, "did:web:"); ok {
+
return fmt.Sprintf("https://%s/.well-known/did.json", after), nil
} else {
return "", fmt.Errorf("did was not a supported did type")
}
-1
identity/passport.go
···
}
}
-
// TODO: should coalesce requests here
doc, err := FetchDidDoc(ctx, p.h, did)
if err != nil {
return nil, err
···
}
}
doc, err := FetchDidDoc(ctx, p.h, did)
if err != nil {
return nil, err
+1 -1
identity/types.go
···
Context []string `json:"@context"`
Id string `json:"id"`
AlsoKnownAs []string `json:"alsoKnownAs"`
-
VerificationMethods []DidDocVerificationMethod `json:"verificationMethods"`
Service []DidDocService `json:"service"`
}
···
Context []string `json:"@context"`
Id string `json:"id"`
AlsoKnownAs []string `json:"alsoKnownAs"`
+
VerificationMethods []DidDocVerificationMethod `json:"verificationMethod"`
Service []DidDocService `json:"service"`
}
+34
init-keys.sh
···
···
+
#!/bin/sh
+
set -e
+
+
mkdir -p /keys
+
mkdir -p /data/cocoon
+
+
if [ ! -f /keys/rotation.key ]; then
+
echo "Generating rotation key..."
+
/cocoon create-rotation-key --out /keys/rotation.key 2>/dev/null || true
+
if [ -f /keys/rotation.key ]; then
+
echo "โœ“ Rotation key generated at /keys/rotation.key"
+
else
+
echo "โœ— Failed to generate rotation key"
+
exit 1
+
fi
+
else
+
echo "โœ“ Rotation key already exists"
+
fi
+
+
if [ ! -f /keys/jwk.key ]; then
+
echo "Generating JWK..."
+
/cocoon create-private-jwk --out /keys/jwk.key 2>/dev/null || true
+
if [ -f /keys/jwk.key ]; then
+
echo "โœ“ JWK generated at /keys/jwk.key"
+
else
+
echo "โœ— Failed to generate JWK"
+
exit 1
+
fi
+
else
+
echo "โœ“ JWK already exists"
+
fi
+
+
echo ""
+
echo "โœ“ Key initialization complete!"
+6
internal/db/db.go
···
return db.cli.Clauses(clauses...).Create(value)
}
func (db *DB) Exec(sql string, clauses []clause.Expression, values ...any) *gorm.DB {
db.mu.Lock()
defer db.mu.Unlock()
···
return db.cli.Clauses(clauses...).Create(value)
}
+
func (db *DB) Save(value any, clauses []clause.Expression) *gorm.DB {
+
db.mu.Lock()
+
defer db.mu.Unlock()
+
return db.cli.Clauses(clauses...).Save(value)
+
}
+
func (db *DB) Exec(sql string, clauses []clause.Expression, values ...any) *gorm.DB {
db.mu.Lock()
defer db.mu.Unlock()
+16
internal/helpers/helpers.go
···
return genericError(e, 400, msg)
}
func InvalidTokenError(e echo.Context) error {
return InputError(e, to.StringPtr("InvalidToken"))
}
···
return genericError(e, 400, msg)
}
+
func UnauthorizedError(e echo.Context, suffix *string) error {
+
msg := "Unauthorized"
+
if suffix != nil {
+
msg += ". " + *suffix
+
}
+
return genericError(e, 401, msg)
+
}
+
+
func ForbiddenError(e echo.Context, suffix *string) error {
+
msg := "Forbidden"
+
if suffix != nil {
+
msg += ". " + *suffix
+
}
+
return genericError(e, 403, msg)
+
}
+
func InvalidTokenError(e echo.Context) error {
return InputError(e, to.StringPtr("InvalidToken"))
}
+19 -2
models/models.go
···
"context"
"time"
-
"github.com/bluesky-social/indigo/atproto/crypto"
)
type Repo struct {
···
EmailUpdateCodeExpiresAt *time.Time
PasswordResetCode *string
PasswordResetCodeExpiresAt *time.Time
Password string
SigningKey []byte
Rev string
Root []byte
Preferences []byte
}
func (r *Repo) SignFor(ctx context.Context, did string, msg []byte) ([]byte, error) {
-
k, err := crypto.ParsePrivateBytesK256(r.SigningKey)
if err != nil {
return nil, err
}
···
}
return sig, nil
}
type Actor struct {
···
Did string `gorm:"index;index:idx_blob_did_cid"`
Cid []byte `gorm:"index;index:idx_blob_did_cid"`
RefCount int
}
type BlobPart struct {
···
"context"
"time"
+
"github.com/Azure/go-autorest/autorest/to"
+
"github.com/bluesky-social/indigo/atproto/atcrypto"
)
type Repo struct {
···
EmailUpdateCodeExpiresAt *time.Time
PasswordResetCode *string
PasswordResetCodeExpiresAt *time.Time
+
PlcOperationCode *string
+
PlcOperationCodeExpiresAt *time.Time
Password string
SigningKey []byte
Rev string
Root []byte
Preferences []byte
+
Deactivated bool
}
func (r *Repo) SignFor(ctx context.Context, did string, msg []byte) ([]byte, error) {
+
k, err := atcrypto.ParsePrivateBytesK256(r.SigningKey)
if err != nil {
return nil, err
}
···
}
return sig, nil
+
}
+
+
func (r *Repo) Status() *string {
+
var status *string
+
if r.Deactivated {
+
status = to.StringPtr("deactivated")
+
}
+
return status
+
}
+
+
func (r *Repo) Active() bool {
+
return r.Status() == nil
}
type Actor struct {
···
Did string `gorm:"index;index:idx_blob_did_cid"`
Cid []byte `gorm:"index;index:idx_blob_did_cid"`
RefCount int
+
Storage string `gorm:"default:sqlite"`
}
type BlobPart struct {
+47 -23
oauth/client/manager.go
···
cli *http.Client
logger *slog.Logger
jwksCache cache.Cache[string, jwk.Key]
-
metadataCache cache.Cache[string, Metadata]
}
type ManagerArgs struct {
···
}
jwksCache := cache.NewCache[string, jwk.Key]().WithLRU().WithMaxKeys(500).WithTTL(5 * time.Minute)
-
metadataCache := cache.NewCache[string, Metadata]().WithLRU().WithMaxKeys(500).WithTTL(5 * time.Minute)
return &Manager{
cli: args.Cli,
···
}
var jwks jwk.Key
-
if metadata.JWKS != nil {
-
// TODO: this is kinda bad but whatever for now. there could obviously be more than one jwk, and we need to
-
// make sure we use the right one
-
k, err := helpers.ParseJWKFromBytes((*metadata.JWKS)[0])
-
if err != nil {
-
return nil, err
-
}
-
jwks = k
-
} else if metadata.JWKSURI != nil {
-
maybeJwks, err := cm.getClientJwks(ctx, clientId, *metadata.JWKSURI)
-
if err != nil {
-
return nil, err
-
}
-
jwks = maybeJwks
}
return &Client{
···
}
func (cm *Manager) getClientMetadata(ctx context.Context, clientId string) (*Metadata, error) {
-
metadataCached, ok := cm.metadataCache.Get(clientId)
if !ok {
req, err := http.NewRequestWithContext(ctx, "GET", clientId, nil)
if err != nil {
···
return nil, err
}
return validated, nil
} else {
-
return &metadataCached, nil
}
}
···
return nil, fmt.Errorf("error unmarshaling metadata: %w", err)
}
u, err := url.Parse(metadata.ClientURI)
if err != nil {
return nil, fmt.Errorf("unable to parse client uri: %w", err)
}
if isLocalHostname(u.Hostname()) {
-
return nil, errors.New("`client_uri` hostname is invalid")
}
if metadata.Scope == "" {
···
return nil, errors.New("private_key_jwt auth method requires jwks or jwks_uri")
}
-
if metadata.JWKS != nil && len(*metadata.JWKS) == 0 {
return nil, errors.New("private_key_jwt auth method requires atleast one key in jwks")
}
···
if u.Scheme != "http" {
return nil, fmt.Errorf("loopback redirect uri %s must use http", ruri)
}
-
-
break
case u.Scheme == "http":
return nil, errors.New("only loopbvack redirect uris are allowed to use the `http` scheme")
case u.Scheme == "https":
if isLocalHostname(u.Hostname()) {
return nil, fmt.Errorf("redirect uri %s's domain must not be a local hostname", ruri)
}
-
break
case strings.Contains(u.Scheme, "."):
if metadata.ApplicationType != "native" {
return nil, errors.New("private-use uri scheme redirect uris are only allowed for native apps")
···
cli *http.Client
logger *slog.Logger
jwksCache cache.Cache[string, jwk.Key]
+
metadataCache cache.Cache[string, *Metadata]
}
type ManagerArgs struct {
···
}
jwksCache := cache.NewCache[string, jwk.Key]().WithLRU().WithMaxKeys(500).WithTTL(5 * time.Minute)
+
metadataCache := cache.NewCache[string, *Metadata]().WithLRU().WithMaxKeys(500).WithTTL(5 * time.Minute)
return &Manager{
cli: args.Cli,
···
}
var jwks jwk.Key
+
if metadata.TokenEndpointAuthMethod == "private_key_jwt" {
+
if metadata.JWKS != nil && len(metadata.JWKS.Keys) > 0 {
+
// TODO: this is kinda bad but whatever for now. there could obviously be more than one jwk, and we need to
+
// make sure we use the right one
+
b, err := json.Marshal(metadata.JWKS.Keys[0])
+
if err != nil {
+
return nil, err
+
}
+
+
k, err := helpers.ParseJWKFromBytes(b)
+
if err != nil {
+
return nil, err
+
}
+
jwks = k
+
} else if metadata.JWKS != nil {
+
} else if metadata.JWKSURI != nil {
+
maybeJwks, err := cm.getClientJwks(ctx, clientId, *metadata.JWKSURI)
+
if err != nil {
+
return nil, err
+
}
+
+
jwks = maybeJwks
+
} else {
+
return nil, fmt.Errorf("no valid jwks found in oauth client metadata")
+
}
}
return &Client{
···
}
func (cm *Manager) getClientMetadata(ctx context.Context, clientId string) (*Metadata, error) {
+
cached, ok := cm.metadataCache.Get(clientId)
if !ok {
req, err := http.NewRequestWithContext(ctx, "GET", clientId, nil)
if err != nil {
···
return nil, err
}
+
cm.metadataCache.Set(clientId, validated, 10*time.Minute)
+
return validated, nil
} else {
+
return cached, nil
}
}
···
return nil, fmt.Errorf("error unmarshaling metadata: %w", err)
}
+
if metadata.ClientURI == "" {
+
u, err := url.Parse(metadata.ClientID)
+
if err != nil {
+
return nil, fmt.Errorf("unable to parse client id: %w", err)
+
}
+
u.RawPath = ""
+
u.RawQuery = ""
+
metadata.ClientURI = u.String()
+
}
+
u, err := url.Parse(metadata.ClientURI)
if err != nil {
return nil, fmt.Errorf("unable to parse client uri: %w", err)
}
+
if metadata.ClientName == "" {
+
metadata.ClientName = metadata.ClientURI
+
}
+
if isLocalHostname(u.Hostname()) {
+
return nil, fmt.Errorf("`client_uri` hostname is invalid: %s", u.Hostname())
}
if metadata.Scope == "" {
···
return nil, errors.New("private_key_jwt auth method requires jwks or jwks_uri")
}
+
if metadata.JWKS != nil && len(metadata.JWKS.Keys) == 0 {
return nil, errors.New("private_key_jwt auth method requires atleast one key in jwks")
}
···
if u.Scheme != "http" {
return nil, fmt.Errorf("loopback redirect uri %s must use http", ruri)
}
case u.Scheme == "http":
return nil, errors.New("only loopbvack redirect uris are allowed to use the `http` scheme")
case u.Scheme == "https":
if isLocalHostname(u.Hostname()) {
return nil, fmt.Errorf("redirect uri %s's domain must not be a local hostname", ruri)
}
case strings.Contains(u.Scheme, "."):
if metadata.ApplicationType != "native" {
return nil, errors.New("private-use uri scheme redirect uris are only allowed for native apps")
+20 -16
oauth/client/metadata.go
···
package client
type Metadata struct {
-
ClientID string `json:"client_id"`
-
ClientName string `json:"client_name"`
-
ClientURI string `json:"client_uri"`
-
LogoURI string `json:"logo_uri"`
-
TOSURI string `json:"tos_uri"`
-
PolicyURI string `json:"policy_uri"`
-
RedirectURIs []string `json:"redirect_uris"`
-
GrantTypes []string `json:"grant_types"`
-
ResponseTypes []string `json:"response_types"`
-
ApplicationType string `json:"application_type"`
-
DpopBoundAccessTokens bool `json:"dpop_bound_access_tokens"`
-
JWKSURI *string `json:"jwks_uri,omitempty"`
-
JWKS *[][]byte `json:"jwks,omitempty"`
-
Scope string `json:"scope"`
-
TokenEndpointAuthMethod string `json:"token_endpoint_auth_method"`
-
TokenEndpointAuthSigningAlg string `json:"token_endpoint_auth_signing_alg"`
}
···
package client
type Metadata struct {
+
ClientID string `json:"client_id"`
+
ClientName string `json:"client_name"`
+
ClientURI string `json:"client_uri"`
+
LogoURI string `json:"logo_uri"`
+
TOSURI string `json:"tos_uri"`
+
PolicyURI string `json:"policy_uri"`
+
RedirectURIs []string `json:"redirect_uris"`
+
GrantTypes []string `json:"grant_types"`
+
ResponseTypes []string `json:"response_types"`
+
ApplicationType string `json:"application_type"`
+
DpopBoundAccessTokens bool `json:"dpop_bound_access_tokens"`
+
JWKSURI *string `json:"jwks_uri,omitempty"`
+
JWKS *MetadataJwks `json:"jwks,omitempty"`
+
Scope string `json:"scope"`
+
TokenEndpointAuthMethod string `json:"token_endpoint_auth_method"`
+
TokenEndpointAuthSigningAlg string `json:"token_endpoint_auth_signing_alg"`
+
}
+
+
type MetadataJwks struct {
+
Keys []any `json:"keys"`
}
+6 -2
oauth/dpop/manager.go
···
Hostname string
}
func NewManager(args ManagerArgs) *Manager {
if args.Logger == nil {
args.Logger = slog.Default()
···
nonce, _ := claims["nonce"].(string)
if nonce == "" {
// WARN: this _must_ be `use_dpop_nonce` for clients know they should make another request
-
return nil, errors.New("use_dpop_nonce")
}
if nonce != "" && !dm.nonce.Check(nonce) {
// WARN: this _must_ be `use_dpop_nonce` so that clients will fetch a new nonce
-
return nil, errors.New("use_dpop_nonce")
}
ath, _ := claims["ath"].(string)
···
Hostname string
}
+
var (
+
ErrUseDpopNonce = errors.New("use_dpop_nonce")
+
)
+
func NewManager(args ManagerArgs) *Manager {
if args.Logger == nil {
args.Logger = slog.Default()
···
nonce, _ := claims["nonce"].(string)
if nonce == "" {
// WARN: this _must_ be `use_dpop_nonce` for clients know they should make another request
+
return nil, ErrUseDpopNonce
}
if nonce != "" && !dm.nonce.Check(nonce) {
// WARN: this _must_ be `use_dpop_nonce` so that clients will fetch a new nonce
+
return nil, ErrUseDpopNonce
}
ath, _ := claims["ath"].(string)
+36 -20
plc/client.go
···
"net/url"
"strings"
-
"github.com/bluesky-social/indigo/atproto/crypto"
"github.com/bluesky-social/indigo/util"
"github.com/haileyok/cocoon/identity"
)
···
h *http.Client
service string
pdsHostname string
-
rotationKey *crypto.PrivateKeyK256
}
type ClientArgs struct {
···
args.H = util.RobustHTTPClient()
}
-
rk, err := crypto.ParsePrivateBytesK256([]byte(args.RotationKey))
if err != nil {
return nil, err
}
···
}, nil
}
-
func (c *Client) CreateDID(sigkey *crypto.PrivateKeyK256, recovery string, handle string) (string, *Operation, error) {
-
pubsigkey, err := sigkey.PublicKey()
if err != nil {
return "", nil, err
}
-
pubrotkey, err := c.rotationKey.PublicKey()
if err != nil {
return "", nil, err
}
// todo
rotationKeys := []string{pubrotkey.DIDKey()}
if recovery != "" {
···
}(recovery)
}
-
op := Operation{
-
Type: "plc_operation",
VerificationMethods: map[string]string{
"atproto": pubsigkey.DIDKey(),
},
···
Endpoint: "https://" + c.pdsHostname,
},
},
-
Prev: nil,
}
-
if err := c.SignOp(sigkey, &op); err != nil {
-
return "", nil, err
-
}
-
-
did, err := DidFromOp(&op)
-
if err != nil {
-
return "", nil, err
-
}
-
-
return did, &op, nil
}
-
func (c *Client) SignOp(sigkey *crypto.PrivateKeyK256, op *Operation) error {
b, err := op.MarshalCBOR()
if err != nil {
return err
···
"net/url"
"strings"
+
"github.com/bluesky-social/indigo/atproto/atcrypto"
"github.com/bluesky-social/indigo/util"
"github.com/haileyok/cocoon/identity"
)
···
h *http.Client
service string
pdsHostname string
+
rotationKey *atcrypto.PrivateKeyK256
}
type ClientArgs struct {
···
args.H = util.RobustHTTPClient()
}
+
rk, err := atcrypto.ParsePrivateBytesK256([]byte(args.RotationKey))
if err != nil {
return nil, err
}
···
}, nil
}
+
func (c *Client) CreateDID(sigkey *atcrypto.PrivateKeyK256, recovery string, handle string) (string, *Operation, error) {
+
creds, err := c.CreateDidCredentials(sigkey, recovery, handle)
if err != nil {
return "", nil, err
}
+
op := Operation{
+
Type: "plc_operation",
+
VerificationMethods: creds.VerificationMethods,
+
RotationKeys: creds.RotationKeys,
+
AlsoKnownAs: creds.AlsoKnownAs,
+
Services: creds.Services,
+
Prev: nil,
+
}
+
+
if err := c.SignOp(sigkey, &op); err != nil {
+
return "", nil, err
+
}
+
+
did, err := DidFromOp(&op)
if err != nil {
return "", nil, err
}
+
return did, &op, nil
+
}
+
+
func (c *Client) CreateDidCredentials(sigkey *atcrypto.PrivateKeyK256, recovery string, handle string) (*DidCredentials, error) {
+
pubsigkey, err := sigkey.PublicKey()
+
if err != nil {
+
return nil, err
+
}
+
+
pubrotkey, err := c.rotationKey.PublicKey()
+
if err != nil {
+
return nil, err
+
}
+
// todo
rotationKeys := []string{pubrotkey.DIDKey()}
if recovery != "" {
···
}(recovery)
}
+
creds := DidCredentials{
VerificationMethods: map[string]string{
"atproto": pubsigkey.DIDKey(),
},
···
Endpoint: "https://" + c.pdsHostname,
},
},
}
+
return &creds, nil
}
+
func (c *Client) SignOp(sigkey *atcrypto.PrivateKeyK256, op *Operation) error {
b, err := op.MarshalCBOR()
if err != nil {
return err
+10 -2
plc/types.go
···
import (
"encoding/json"
-
"github.com/bluesky-social/indigo/atproto/data"
"github.com/haileyok/cocoon/identity"
cbg "github.com/whyrusleeping/cbor-gen"
)
type Operation struct {
Type string `json:"type"`
···
return nil, err
}
-
b, err = data.MarshalCBOR(m)
if err != nil {
return nil, err
}
···
import (
"encoding/json"
+
"github.com/bluesky-social/indigo/atproto/atdata"
"github.com/haileyok/cocoon/identity"
cbg "github.com/whyrusleeping/cbor-gen"
)
+
+
+
type DidCredentials struct {
+
VerificationMethods map[string]string `json:"verificationMethods"`
+
RotationKeys []string `json:"rotationKeys"`
+
AlsoKnownAs []string `json:"alsoKnownAs"`
+
Services map[string]identity.OperationService `json:"services"`
+
}
type Operation struct {
Type string `json:"type"`
···
return nil, err
}
+
b, err = atdata.MarshalCBOR(m)
if err != nil {
return nil, err
}
+1
recording_blockstore/recording_blockstore.go
···
return &RecordingBlockstore{
base: base,
inserts: make(map[cid.Cid]blockformat.Block),
}
}
···
return &RecordingBlockstore{
base: base,
inserts: make(map[cid.Cid]blockformat.Block),
+
reads: make(map[cid.Cid]blockformat.Block),
}
}
+1
server/handle_account.go
···
func (s *Server) handleAccount(e echo.Context) error {
ctx := e.Request().Context()
repo, sess, err := s.getSessionRepoOrErr(e)
if err != nil {
return e.Redirect(303, "/account/signin")
···
func (s *Server) handleAccount(e echo.Context) error {
ctx := e.Request().Context()
+
repo, sess, err := s.getSessionRepoOrErr(e)
if err != nil {
return e.Redirect(303, "/account/signin")
+1 -1
server/handle_actor_get_preferences.go
···
err := json.Unmarshal(repo.Preferences, &prefs)
if err != nil || prefs["preferences"] == nil {
prefs = map[string]any{
-
"preferences": map[string]any{},
}
}
···
err := json.Unmarshal(repo.Preferences, &prefs)
if err != nil || prefs["preferences"] == nil {
prefs = map[string]any{
+
"preferences": []any{},
}
}
+29
server/handle_identity_request_plc_operation.go
···
···
+
package server
+
+
import (
+
"fmt"
+
"time"
+
+
"github.com/haileyok/cocoon/internal/helpers"
+
"github.com/haileyok/cocoon/models"
+
"github.com/labstack/echo/v4"
+
)
+
+
func (s *Server) handleIdentityRequestPlcOperationSignature(e echo.Context) error {
+
urepo := e.Get("repo").(*models.RepoActor)
+
+
code := fmt.Sprintf("%s-%s", helpers.RandomVarchar(5), helpers.RandomVarchar(5))
+
eat := time.Now().Add(10 * time.Minute).UTC()
+
+
if err := s.db.Exec("UPDATE repos SET plc_operation_code = ?, plc_operation_code_expires_at = ? WHERE did = ?", nil, code, eat, urepo.Repo.Did).Error; err != nil {
+
s.logger.Error("error updating user", "error", err)
+
return helpers.ServerError(e, nil)
+
}
+
+
if err := s.sendPlcTokenReset(urepo.Email, urepo.Handle, code); err != nil {
+
s.logger.Error("error sending mail", "error", err)
+
return helpers.ServerError(e, nil)
+
}
+
+
return e.NoContent(200)
+
}
+103
server/handle_identity_sign_plc_operation.go
···
···
+
package server
+
+
import (
+
"context"
+
"strings"
+
"time"
+
+
"github.com/Azure/go-autorest/autorest/to"
+
"github.com/bluesky-social/indigo/atproto/atcrypto"
+
"github.com/haileyok/cocoon/identity"
+
"github.com/haileyok/cocoon/internal/helpers"
+
"github.com/haileyok/cocoon/models"
+
"github.com/haileyok/cocoon/plc"
+
"github.com/labstack/echo/v4"
+
)
+
+
type ComAtprotoSignPlcOperationRequest struct {
+
Token string `json:"token"`
+
VerificationMethods *map[string]string `json:"verificationMethods"`
+
RotationKeys *[]string `json:"rotationKeys"`
+
AlsoKnownAs *[]string `json:"alsoKnownAs"`
+
Services *map[string]identity.OperationService `json:"services"`
+
}
+
+
type ComAtprotoSignPlcOperationResponse struct {
+
Operation plc.Operation `json:"operation"`
+
}
+
+
func (s *Server) handleSignPlcOperation(e echo.Context) error {
+
repo := e.Get("repo").(*models.RepoActor)
+
+
var req ComAtprotoSignPlcOperationRequest
+
if err := e.Bind(&req); err != nil {
+
s.logger.Error("error binding", "error", err)
+
return helpers.ServerError(e, nil)
+
}
+
+
if !strings.HasPrefix(repo.Repo.Did, "did:plc:") {
+
return helpers.InputError(e, nil)
+
}
+
+
if repo.PlcOperationCode == nil || repo.PlcOperationCodeExpiresAt == nil {
+
return helpers.InputError(e, to.StringPtr("InvalidToken"))
+
}
+
+
if *repo.PlcOperationCode != req.Token {
+
return helpers.InvalidTokenError(e)
+
}
+
+
if time.Now().UTC().After(*repo.PlcOperationCodeExpiresAt) {
+
return helpers.ExpiredTokenError(e)
+
}
+
+
ctx := context.WithValue(e.Request().Context(), "skip-cache", true)
+
log, err := identity.FetchDidAuditLog(ctx, nil, repo.Repo.Did)
+
if err != nil {
+
s.logger.Error("error fetching doc", "error", err)
+
return helpers.ServerError(e, nil)
+
}
+
+
latest := log[len(log)-1]
+
+
op := plc.Operation{
+
Type: "plc_operation",
+
VerificationMethods: latest.Operation.VerificationMethods,
+
RotationKeys: latest.Operation.RotationKeys,
+
AlsoKnownAs: latest.Operation.AlsoKnownAs,
+
Services: latest.Operation.Services,
+
Prev: &latest.Cid,
+
}
+
if req.VerificationMethods != nil {
+
op.VerificationMethods = *req.VerificationMethods
+
}
+
if req.RotationKeys != nil {
+
op.RotationKeys = *req.RotationKeys
+
}
+
if req.AlsoKnownAs != nil {
+
op.AlsoKnownAs = *req.AlsoKnownAs
+
}
+
if req.Services != nil {
+
op.Services = *req.Services
+
}
+
+
k, err := atcrypto.ParsePrivateBytesK256(repo.SigningKey)
+
if err != nil {
+
s.logger.Error("error parsing signing key", "error", err)
+
return helpers.ServerError(e, nil)
+
}
+
+
if err := s.plcClient.SignOp(k, &op); err != nil {
+
s.logger.Error("error signing plc operation", "error", err)
+
return helpers.ServerError(e, nil)
+
}
+
+
if err := s.db.Exec("UPDATE repos SET plc_operation_code = NULL, plc_operation_code_expires_at = NULL WHERE did = ?", nil, repo.Repo.Did).Error; err != nil {
+
s.logger.Error("error updating repo", "error", err)
+
return helpers.ServerError(e, nil)
+
}
+
+
return e.JSON(200, ComAtprotoSignPlcOperationResponse{
+
Operation: op,
+
})
+
}
+87
server/handle_identity_submit_plc_operation.go
···
···
+
package server
+
+
import (
+
"context"
+
"slices"
+
"strings"
+
"time"
+
+
"github.com/bluesky-social/indigo/api/atproto"
+
"github.com/bluesky-social/indigo/atproto/atcrypto"
+
"github.com/bluesky-social/indigo/events"
+
"github.com/bluesky-social/indigo/util"
+
"github.com/haileyok/cocoon/internal/helpers"
+
"github.com/haileyok/cocoon/models"
+
"github.com/haileyok/cocoon/plc"
+
"github.com/labstack/echo/v4"
+
)
+
+
type ComAtprotoSubmitPlcOperationRequest struct {
+
Operation plc.Operation `json:"operation"`
+
}
+
+
func (s *Server) handleSubmitPlcOperation(e echo.Context) error {
+
repo := e.Get("repo").(*models.RepoActor)
+
+
var req ComAtprotoSubmitPlcOperationRequest
+
if err := e.Bind(&req); err != nil {
+
s.logger.Error("error binding", "error", err)
+
return helpers.ServerError(e, nil)
+
}
+
+
if err := e.Validate(req); err != nil {
+
return helpers.InputError(e, nil)
+
}
+
if !strings.HasPrefix(repo.Repo.Did, "did:plc:") {
+
return helpers.InputError(e, nil)
+
}
+
+
op := req.Operation
+
+
k, err := atcrypto.ParsePrivateBytesK256(repo.SigningKey)
+
if err != nil {
+
s.logger.Error("error parsing key", "error", err)
+
return helpers.ServerError(e, nil)
+
}
+
required, err := s.plcClient.CreateDidCredentials(k, "", repo.Actor.Handle)
+
if err != nil {
+
s.logger.Error("error crating did credentials", "error", err)
+
return helpers.ServerError(e, nil)
+
}
+
+
for _, expectedKey := range required.RotationKeys {
+
if !slices.Contains(op.RotationKeys, expectedKey) {
+
return helpers.InputError(e, nil)
+
}
+
}
+
if op.Services["atproto_pds"].Type != "AtprotoPersonalDataServer" {
+
return helpers.InputError(e, nil)
+
}
+
if op.Services["atproto_pds"].Endpoint != required.Services["atproto_pds"].Endpoint {
+
return helpers.InputError(e, nil)
+
}
+
if op.VerificationMethods["atproto"] != required.VerificationMethods["atproto"] {
+
return helpers.InputError(e, nil)
+
}
+
if op.AlsoKnownAs[0] != required.AlsoKnownAs[0] {
+
return helpers.InputError(e, nil)
+
}
+
+
if err := s.plcClient.SendOperation(e.Request().Context(), repo.Repo.Did, &op); err != nil {
+
return err
+
}
+
+
if err := s.passport.BustDoc(context.TODO(), repo.Repo.Did); err != nil {
+
s.logger.Warn("error busting did doc", "error", err)
+
}
+
+
s.evtman.AddEvent(context.TODO(), &events.XRPCStreamEvent{
+
RepoIdentity: &atproto.SyncSubscribeRepos_Identity{
+
Did: repo.Repo.Did,
+
Seq: time.Now().UnixMicro(), // TODO: no
+
Time: time.Now().Format(util.ISO8601),
+
},
+
})
+
+
return nil
+
}
+2 -11
server/handle_identity_update_handle.go
···
"github.com/Azure/go-autorest/autorest/to"
"github.com/bluesky-social/indigo/api/atproto"
-
"github.com/bluesky-social/indigo/atproto/crypto"
"github.com/bluesky-social/indigo/events"
"github.com/bluesky-social/indigo/util"
"github.com/haileyok/cocoon/identity"
···
Prev: &latest.Cid,
}
-
k, err := crypto.ParsePrivateBytesK256(repo.SigningKey)
if err != nil {
s.logger.Error("error parsing signing key", "error", err)
return helpers.ServerError(e, nil)
···
if err := s.passport.BustDoc(context.TODO(), repo.Repo.Did); err != nil {
s.logger.Warn("error busting did doc", "error", err)
}
-
-
s.evtman.AddEvent(context.TODO(), &events.XRPCStreamEvent{
-
RepoHandle: &atproto.SyncSubscribeRepos_Handle{
-
Did: repo.Repo.Did,
-
Handle: req.Handle,
-
Seq: time.Now().UnixMicro(), // TODO: no
-
Time: time.Now().Format(util.ISO8601),
-
},
-
})
s.evtman.AddEvent(context.TODO(), &events.XRPCStreamEvent{
RepoIdentity: &atproto.SyncSubscribeRepos_Identity{
···
"github.com/Azure/go-autorest/autorest/to"
"github.com/bluesky-social/indigo/api/atproto"
+
"github.com/bluesky-social/indigo/atproto/atcrypto"
"github.com/bluesky-social/indigo/events"
"github.com/bluesky-social/indigo/util"
"github.com/haileyok/cocoon/identity"
···
Prev: &latest.Cid,
}
+
k, err := atcrypto.ParsePrivateBytesK256(repo.SigningKey)
if err != nil {
s.logger.Error("error parsing signing key", "error", err)
return helpers.ServerError(e, nil)
···
if err := s.passport.BustDoc(context.TODO(), repo.Repo.Did); err != nil {
s.logger.Warn("error busting did doc", "error", err)
}
s.evtman.AddEvent(context.TODO(), &events.XRPCStreamEvent{
RepoIdentity: &atproto.SyncSubscribeRepos_Identity{
+9 -8
server/handle_import_repo.go
···
import (
"bytes"
-
"context"
"io"
"slices"
"strings"
···
)
func (s *Server) handleRepoImportRepo(e echo.Context) error {
urepo := e.Get("repo").(*models.RepoActor)
b, err := io.ReadAll(e.Request().Body)
···
slices.Reverse(orderedBlocks)
-
if err := bs.PutMany(context.TODO(), orderedBlocks); err != nil {
s.logger.Error("could not insert blocks", "error", err)
return helpers.ServerError(e, nil)
}
-
r, err := repo.OpenRepo(context.TODO(), bs, cs.Header.Roots[0])
if err != nil {
s.logger.Error("could not open repo", "error", err)
return helpers.ServerError(e, nil)
···
clock := syntax.NewTIDClock(0)
-
if err := r.ForEach(context.TODO(), "", func(key string, cid cid.Cid) error {
pts := strings.Split(key, "/")
nsid := pts[0]
rkey := pts[1]
cidStr := cid.String()
-
b, err := bs.Get(context.TODO(), cid)
if err != nil {
s.logger.Error("record bytes don't exist in blockstore", "error", err)
return helpers.ServerError(e, nil)
···
Value: b.RawData(),
}
-
if err := tx.Create(rec).Error; err != nil {
return err
}
···
tx.Commit()
-
root, rev, err := r.Commit(context.TODO(), urepo.SignFor)
if err != nil {
s.logger.Error("error committing", "error", err)
return helpers.ServerError(e, nil)
}
-
if err := s.UpdateRepo(context.TODO(), urepo.Repo.Did, root, rev); err != nil {
s.logger.Error("error updating repo after commit", "error", err)
return helpers.ServerError(e, nil)
}
···
import (
"bytes"
"io"
"slices"
"strings"
···
)
func (s *Server) handleRepoImportRepo(e echo.Context) error {
+
ctx := e.Request().Context()
+
urepo := e.Get("repo").(*models.RepoActor)
b, err := io.ReadAll(e.Request().Body)
···
slices.Reverse(orderedBlocks)
+
if err := bs.PutMany(ctx, orderedBlocks); err != nil {
s.logger.Error("could not insert blocks", "error", err)
return helpers.ServerError(e, nil)
}
+
r, err := repo.OpenRepo(ctx, bs, cs.Header.Roots[0])
if err != nil {
s.logger.Error("could not open repo", "error", err)
return helpers.ServerError(e, nil)
···
clock := syntax.NewTIDClock(0)
+
if err := r.ForEach(ctx, "", func(key string, cid cid.Cid) error {
pts := strings.Split(key, "/")
nsid := pts[0]
rkey := pts[1]
cidStr := cid.String()
+
b, err := bs.Get(ctx, cid)
if err != nil {
s.logger.Error("record bytes don't exist in blockstore", "error", err)
return helpers.ServerError(e, nil)
···
Value: b.RawData(),
}
+
if err := tx.Save(rec).Error; err != nil {
return err
}
···
tx.Commit()
+
root, rev, err := r.Commit(ctx, urepo.SignFor)
if err != nil {
s.logger.Error("error committing", "error", err)
return helpers.ServerError(e, nil)
}
+
if err := s.UpdateRepo(ctx, urepo.Repo.Did, root, rev); err != nil {
s.logger.Error("error updating repo after commit", "error", err)
return helpers.ServerError(e, nil)
}
+9 -2
server/handle_oauth_par.go
···
package server
import (
"time"
"github.com/Azure/go-autorest/autorest/to"
"github.com/haileyok/cocoon/internal/helpers"
"github.com/haileyok/cocoon/oauth"
"github.com/haileyok/cocoon/oauth/constants"
"github.com/haileyok/cocoon/oauth/provider"
"github.com/labstack/echo/v4"
)
···
// TODO: this seems wrong. should be a way to get the entire request url i believe, but this will work for now
dpopProof, err := s.oauthProvider.DpopManager.CheckProof(e.Request().Method, "https://"+s.config.Hostname+e.Request().URL.String(), e.Request().Header, nil)
if err != nil {
s.logger.Error("error getting dpop proof", "error", err)
-
return helpers.InputError(e, to.StringPtr(err.Error()))
}
client, clientAuth, err := s.oauthProvider.AuthenticateClient(e.Request().Context(), parRequest.AuthenticateClientRequestBase, dpopProof, &provider.AuthenticateClientOptions{
···
AllowMissingDpopProof: true,
})
if err != nil {
-
s.logger.Error("error authenticating client", "error", err)
return helpers.InputError(e, to.StringPtr(err.Error()))
}
···
package server
import (
+
"errors"
"time"
"github.com/Azure/go-autorest/autorest/to"
"github.com/haileyok/cocoon/internal/helpers"
"github.com/haileyok/cocoon/oauth"
"github.com/haileyok/cocoon/oauth/constants"
+
"github.com/haileyok/cocoon/oauth/dpop"
"github.com/haileyok/cocoon/oauth/provider"
"github.com/labstack/echo/v4"
)
···
// TODO: this seems wrong. should be a way to get the entire request url i believe, but this will work for now
dpopProof, err := s.oauthProvider.DpopManager.CheckProof(e.Request().Method, "https://"+s.config.Hostname+e.Request().URL.String(), e.Request().Header, nil)
if err != nil {
+
if errors.Is(err, dpop.ErrUseDpopNonce) {
+
return e.JSON(400, map[string]string{
+
"error": "use_dpop_nonce",
+
})
+
}
s.logger.Error("error getting dpop proof", "error", err)
+
return helpers.InputError(e, nil)
}
client, clientAuth, err := s.oauthProvider.AuthenticateClient(e.Request().Context(), parRequest.AuthenticateClientRequestBase, dpopProof, &provider.AuthenticateClientOptions{
···
AllowMissingDpopProof: true,
})
if err != nil {
+
s.logger.Error("error authenticating client", "client_id", parRequest.ClientID, "error", err)
return helpers.InputError(e, to.StringPtr(err.Error()))
}
+9 -2
server/handle_oauth_token.go
···
"bytes"
"crypto/sha256"
"encoding/base64"
"fmt"
"slices"
"time"
···
"github.com/haileyok/cocoon/internal/helpers"
"github.com/haileyok/cocoon/oauth"
"github.com/haileyok/cocoon/oauth/constants"
"github.com/haileyok/cocoon/oauth/provider"
"github.com/labstack/echo/v4"
)
···
proof, err := s.oauthProvider.DpopManager.CheckProof(e.Request().Method, e.Request().URL.String(), e.Request().Header, nil)
if err != nil {
s.logger.Error("error getting dpop proof", "error", err)
-
return helpers.InputError(e, to.StringPtr(err.Error()))
}
client, clientAuth, err := s.oauthProvider.AuthenticateClient(e.Request().Context(), req.AuthenticateClientRequestBase, proof, &provider.AuthenticateClientOptions{
AllowMissingDpopProof: true,
})
if err != nil {
-
s.logger.Error("error authenticating client", "error", err)
return helpers.InputError(e, to.StringPtr(err.Error()))
}
···
"bytes"
"crypto/sha256"
"encoding/base64"
+
"errors"
"fmt"
"slices"
"time"
···
"github.com/haileyok/cocoon/internal/helpers"
"github.com/haileyok/cocoon/oauth"
"github.com/haileyok/cocoon/oauth/constants"
+
"github.com/haileyok/cocoon/oauth/dpop"
"github.com/haileyok/cocoon/oauth/provider"
"github.com/labstack/echo/v4"
)
···
proof, err := s.oauthProvider.DpopManager.CheckProof(e.Request().Method, e.Request().URL.String(), e.Request().Header, nil)
if err != nil {
+
if errors.Is(err, dpop.ErrUseDpopNonce) {
+
return e.JSON(400, map[string]string{
+
"error": "use_dpop_nonce",
+
})
+
}
s.logger.Error("error getting dpop proof", "error", err)
+
return helpers.InputError(e, nil)
}
client, clientAuth, err := s.oauthProvider.AuthenticateClient(e.Request().Context(), req.AuthenticateClientRequestBase, proof, &provider.AuthenticateClientOptions{
AllowMissingDpopProof: true,
})
if err != nil {
+
s.logger.Error("error authenticating client", "client_id", req.ClientID, "error", err)
return helpers.InputError(e, to.StringPtr(err.Error()))
}
+17 -4
server/handle_proxy.go
···
func (s *Server) getAtprotoProxyEndpointFromRequest(e echo.Context) (string, string, error) {
svc := e.Request().Header.Get("atproto-proxy")
-
if svc == "" {
-
svc = s.config.DefaultAtprotoProxy
}
svcPts := strings.Split(svc, "#")
···
encheader := strings.TrimRight(base64.RawURLEncoding.EncodeToString(hj), "=")
payload := map[string]any{
"iss": repo.Repo.Did,
-
"aud": svcDid,
-
"lxm": pts[2],
"jti": uuid.NewString(),
"exp": time.Now().Add(1 * time.Minute).UTC().Unix(),
}
···
func (s *Server) getAtprotoProxyEndpointFromRequest(e echo.Context) (string, string, error) {
svc := e.Request().Header.Get("atproto-proxy")
+
if svc == "" && s.config.FallbackProxy != "" {
+
svc = s.config.FallbackProxy
}
svcPts := strings.Split(svc, "#")
···
encheader := strings.TrimRight(base64.RawURLEncoding.EncodeToString(hj), "=")
+
// When proxying app.bsky.feed.getFeed the token is actually issued for the
+
// underlying feed generator and the app view passes it on. This allows the
+
// getFeed implementation to pass in the desired lxm and aud for the token
+
// and then just delegate to the general proxying logic
+
lxm, proxyTokenLxmExists := e.Get("proxyTokenLxm").(string)
+
if !proxyTokenLxmExists || lxm == "" {
+
lxm = pts[2]
+
}
+
aud, proxyTokenAudExists := e.Get("proxyTokenAud").(string)
+
if !proxyTokenAudExists || aud == "" {
+
aud = svcDid
+
}
+
payload := map[string]any{
"iss": repo.Repo.Did,
+
"aud": aud,
+
"lxm": lxm,
"jti": uuid.NewString(),
"exp": time.Now().Add(1 * time.Minute).UTC().Unix(),
}
+35
server/handle_proxy_get_feed.go
···
···
+
package server
+
+
import (
+
"github.com/Azure/go-autorest/autorest/to"
+
"github.com/bluesky-social/indigo/api/atproto"
+
"github.com/bluesky-social/indigo/api/bsky"
+
"github.com/bluesky-social/indigo/atproto/syntax"
+
"github.com/bluesky-social/indigo/xrpc"
+
"github.com/haileyok/cocoon/internal/helpers"
+
"github.com/labstack/echo/v4"
+
)
+
+
func (s *Server) handleProxyBskyFeedGetFeed(e echo.Context) error {
+
feedUri, err := syntax.ParseATURI(e.QueryParam("feed"))
+
if err != nil {
+
return helpers.InputError(e, to.StringPtr("invalid feed uri"))
+
}
+
+
appViewEndpoint, _, err := s.getAtprotoProxyEndpointFromRequest(e)
+
if err != nil {
+
e.Logger().Error("could not get atproto proxy", "error", err)
+
return helpers.ServerError(e, nil)
+
}
+
+
appViewClient := xrpc.Client{
+
Host: appViewEndpoint,
+
}
+
feedRecord, err := atproto.RepoGetRecord(e.Request().Context(), &appViewClient, "", feedUri.Collection().String(), feedUri.Authority().String(), feedUri.RecordKey().String())
+
feedGeneratorDid := feedRecord.Value.Val.(*bsky.FeedGenerator).Did
+
+
e.Set("proxyTokenLxm", "app.bsky.feed.getFeedSkeleton")
+
e.Set("proxyTokenAud", feedGeneratorDid)
+
+
return s.handleProxy(e)
+
}
+3 -1
server/handle_repo_apply_writes.go
···
}
func (s *Server) handleApplyWrites(e echo.Context) error {
repo := e.Get("repo").(*models.RepoActor)
var req ComAtprotoRepoApplyWritesRequest
···
})
}
-
results, err := s.repoman.applyWrites(repo.Repo, ops, req.SwapCommit)
if err != nil {
s.logger.Error("error applying writes", "error", err)
return helpers.ServerError(e, nil)
···
}
func (s *Server) handleApplyWrites(e echo.Context) error {
+
ctx := e.Request().Context()
+
repo := e.Get("repo").(*models.RepoActor)
var req ComAtprotoRepoApplyWritesRequest
···
})
}
+
results, err := s.repoman.applyWrites(ctx, repo.Repo, ops, req.SwapCommit)
if err != nil {
s.logger.Error("error applying writes", "error", err)
return helpers.ServerError(e, nil)
+3 -1
server/handle_repo_create_record.go
···
}
func (s *Server) handleCreateRecord(e echo.Context) error {
repo := e.Get("repo").(*models.RepoActor)
var req ComAtprotoRepoCreateRecordRequest
···
optype = OpTypeUpdate
}
-
results, err := s.repoman.applyWrites(repo.Repo, []Op{
{
Type: optype,
Collection: req.Collection,
···
}
func (s *Server) handleCreateRecord(e echo.Context) error {
+
ctx := e.Request().Context()
+
repo := e.Get("repo").(*models.RepoActor)
var req ComAtprotoRepoCreateRecordRequest
···
optype = OpTypeUpdate
}
+
results, err := s.repoman.applyWrites(ctx, repo.Repo, []Op{
{
Type: optype,
Collection: req.Collection,
+3 -1
server/handle_repo_delete_record.go
···
}
func (s *Server) handleDeleteRecord(e echo.Context) error {
repo := e.Get("repo").(*models.RepoActor)
var req ComAtprotoRepoDeleteRecordRequest
···
return helpers.InputError(e, nil)
}
-
results, err := s.repoman.applyWrites(repo.Repo, []Op{
{
Type: OpTypeDelete,
Collection: req.Collection,
···
}
func (s *Server) handleDeleteRecord(e echo.Context) error {
+
ctx := e.Request().Context()
+
repo := e.Get("repo").(*models.RepoActor)
var req ComAtprotoRepoDeleteRecordRequest
···
return helpers.InputError(e, nil)
}
+
results, err := s.repoman.applyWrites(ctx, repo.Repo, []Op{
{
Type: OpTypeDelete,
Collection: req.Collection,
+2 -2
server/handle_repo_get_record.go
···
package server
import (
-
"github.com/bluesky-social/indigo/atproto/data"
"github.com/bluesky-social/indigo/atproto/syntax"
"github.com/haileyok/cocoon/models"
"github.com/labstack/echo/v4"
···
return err
}
-
val, err := data.UnmarshalCBOR(record.Value)
if err != nil {
return s.handleProxy(e) // TODO: this should be getting handled like...if we don't find it in the db. why doesn't it throw error up there?
}
···
package server
import (
+
"github.com/bluesky-social/indigo/atproto/atdata"
"github.com/bluesky-social/indigo/atproto/syntax"
"github.com/haileyok/cocoon/models"
"github.com/labstack/echo/v4"
···
return err
}
+
val, err := atdata.UnmarshalCBOR(record.Value)
if err != nil {
return s.handleProxy(e) // TODO: this should be getting handled like...if we don't find it in the db. why doesn't it throw error up there?
}
+2 -2
server/handle_repo_list_records.go
···
"strconv"
"github.com/Azure/go-autorest/autorest/to"
-
"github.com/bluesky-social/indigo/atproto/data"
"github.com/bluesky-social/indigo/atproto/syntax"
"github.com/haileyok/cocoon/internal/helpers"
"github.com/haileyok/cocoon/models"
···
items := []ComAtprotoRepoListRecordsRecordItem{}
for _, r := range records {
-
val, err := data.UnmarshalCBOR(r.Value)
if err != nil {
return err
}
···
"strconv"
"github.com/Azure/go-autorest/autorest/to"
+
"github.com/bluesky-social/indigo/atproto/atdata"
"github.com/bluesky-social/indigo/atproto/syntax"
"github.com/haileyok/cocoon/internal/helpers"
"github.com/haileyok/cocoon/models"
···
items := []ComAtprotoRepoListRecordsRecordItem{}
for _, r := range records {
+
val, err := atdata.UnmarshalCBOR(r.Value)
if err != nil {
return err
}
+2 -2
server/handle_repo_list_repos.go
···
Did: r.Did,
Head: c.String(),
Rev: r.Rev,
-
Active: true,
-
Status: nil,
})
}
···
Did: r.Did,
Head: c.String(),
Rev: r.Rev,
+
Active: r.Active(),
+
Status: r.Status(),
})
}
+3 -1
server/handle_repo_put_record.go
···
}
func (s *Server) handlePutRecord(e echo.Context) error {
repo := e.Get("repo").(*models.RepoActor)
var req ComAtprotoRepoPutRecordRequest
···
optype = OpTypeUpdate
}
-
results, err := s.repoman.applyWrites(repo.Repo, []Op{
{
Type: optype,
Collection: req.Collection,
···
}
func (s *Server) handlePutRecord(e echo.Context) error {
+
ctx := e.Request().Context()
+
repo := e.Get("repo").(*models.RepoActor)
var req ComAtprotoRepoPutRecordRequest
···
optype = OpTypeUpdate
}
+
results, err := s.repoman.applyWrites(ctx, repo.Repo, []Op{
{
Type: optype,
Collection: req.Collection,
+50 -8
server/handle_repo_upload_blob.go
···
import (
"bytes"
"io"
"github.com/haileyok/cocoon/internal/helpers"
"github.com/haileyok/cocoon/models"
"github.com/ipfs/go-cid"
···
mime = "application/octet-stream"
}
blob := models.Blob{
Did: urepo.Repo.Did,
RefCount: 0,
CreatedAt: s.repoman.clock.Next().String(),
}
if err := s.db.Create(&blob, nil).Error; err != nil {
···
read += n
fulldata.Write(data)
-
blobPart := models.BlobPart{
-
BlobID: blob.ID,
-
Idx: part,
-
Data: data,
-
}
-
if err := s.db.Create(&blobPart, nil).Error; err != nil {
-
s.logger.Error("error adding blob part to db", "error", err)
-
return helpers.ServerError(e, nil)
}
part++
···
if err != nil {
s.logger.Error("error creating cid prefix", "error", err)
return helpers.ServerError(e, nil)
}
if err := s.db.Exec("UPDATE blobs SET cid = ? WHERE id = ?", nil, c.Bytes(), blob.ID).Error; err != nil {
···
import (
"bytes"
+
"fmt"
"io"
+
"github.com/aws/aws-sdk-go/aws"
+
"github.com/aws/aws-sdk-go/aws/credentials"
+
"github.com/aws/aws-sdk-go/aws/session"
+
"github.com/aws/aws-sdk-go/service/s3"
"github.com/haileyok/cocoon/internal/helpers"
"github.com/haileyok/cocoon/models"
"github.com/ipfs/go-cid"
···
mime = "application/octet-stream"
}
+
storage := "sqlite"
+
s3Upload := s.s3Config != nil && s.s3Config.BlobstoreEnabled
+
if s3Upload {
+
storage = "s3"
+
}
blob := models.Blob{
Did: urepo.Repo.Did,
RefCount: 0,
CreatedAt: s.repoman.clock.Next().String(),
+
Storage: storage,
}
if err := s.db.Create(&blob, nil).Error; err != nil {
···
read += n
fulldata.Write(data)
+
if !s3Upload {
+
blobPart := models.BlobPart{
+
BlobID: blob.ID,
+
Idx: part,
+
Data: data,
+
}
+
if err := s.db.Create(&blobPart, nil).Error; err != nil {
+
s.logger.Error("error adding blob part to db", "error", err)
+
return helpers.ServerError(e, nil)
+
}
}
part++
···
if err != nil {
s.logger.Error("error creating cid prefix", "error", err)
return helpers.ServerError(e, nil)
+
}
+
+
if s3Upload {
+
config := &aws.Config{
+
Region: aws.String(s.s3Config.Region),
+
Credentials: credentials.NewStaticCredentials(s.s3Config.AccessKey, s.s3Config.SecretKey, ""),
+
}
+
+
if s.s3Config.Endpoint != "" {
+
config.Endpoint = aws.String(s.s3Config.Endpoint)
+
config.S3ForcePathStyle = aws.Bool(true)
+
}
+
+
sess, err := session.NewSession(config)
+
if err != nil {
+
s.logger.Error("error creating aws session", "error", err)
+
return helpers.ServerError(e, nil)
+
}
+
+
svc := s3.New(sess)
+
+
if _, err := svc.PutObject(&s3.PutObjectInput{
+
Bucket: aws.String(s.s3Config.Bucket),
+
Key: aws.String(fmt.Sprintf("blobs/%s/%s", urepo.Repo.Did, c.String())),
+
Body: bytes.NewReader(fulldata.Bytes()),
+
}); err != nil {
+
s.logger.Error("error uploading blob to s3", "error", err)
+
return helpers.ServerError(e, nil)
+
}
}
if err := s.db.Exec("UPDATE blobs SET cid = ? WHERE id = ?", nil, c.Bytes(), blob.ID).Error; err != nil {
+45
server/handle_server_activate_account.go
···
···
+
package server
+
+
import (
+
"context"
+
"time"
+
+
"github.com/bluesky-social/indigo/api/atproto"
+
"github.com/bluesky-social/indigo/events"
+
"github.com/bluesky-social/indigo/util"
+
"github.com/haileyok/cocoon/internal/helpers"
+
"github.com/haileyok/cocoon/models"
+
"github.com/labstack/echo/v4"
+
)
+
+
type ComAtprotoServerActivateAccountRequest struct {
+
// NOTE: this implementation will not pay attention to this value
+
DeleteAfter time.Time `json:"deleteAfter"`
+
}
+
+
func (s *Server) handleServerActivateAccount(e echo.Context) error {
+
var req ComAtprotoServerDeactivateAccountRequest
+
if err := e.Bind(&req); err != nil {
+
s.logger.Error("error binding", "error", err)
+
return helpers.ServerError(e, nil)
+
}
+
+
urepo := e.Get("repo").(*models.RepoActor)
+
+
if err := s.db.Exec("UPDATE repos SET deactivated = ? WHERE did = ?", nil, false, urepo.Repo.Did).Error; err != nil {
+
s.logger.Error("error updating account status to deactivated", "error", err)
+
return helpers.ServerError(e, nil)
+
}
+
+
s.evtman.AddEvent(context.TODO(), &events.XRPCStreamEvent{
+
RepoAccount: &atproto.SyncSubscribeRepos_Account{
+
Active: true,
+
Did: urepo.Repo.Did,
+
Status: nil,
+
Seq: time.Now().UnixMicro(), // TODO: bad puppy
+
Time: time.Now().Format(util.ISO8601),
+
},
+
})
+
+
return e.NoContent(200)
+
}
+46 -15
server/handle_server_check_account_status.go
···
package server
import (
"github.com/haileyok/cocoon/internal/helpers"
"github.com/haileyok/cocoon/models"
"github.com/ipfs/go-cid"
···
func (s *Server) handleServerCheckAccountStatus(e echo.Context) error {
urepo := e.Get("repo").(*models.RepoActor)
resp := ComAtprotoServerCheckAccountStatusResponse{
Activated: true, // TODO: should allow for deactivation etc.
-
ValidDid: true, // TODO: should probably verify?
RepoRev: urepo.Rev,
ImportedBlobs: 0, // TODO: ???
}
···
s.logger.Error("error casting cid", "error", err)
return helpers.ServerError(e, nil)
}
resp.RepoCommit = rootcid.String()
type CountResp struct {
···
}
var blockCtResp CountResp
-
if err := s.db.Raw("SELECT COUNT(*) AS ct FROM blocks WHERE did = ?", nil, urepo.Repo.Did).Scan(&blockCtResp).Error; err != nil {
-
s.logger.Error("error getting block count", "error", err)
-
return helpers.ServerError(e, nil)
-
}
-
resp.RepoBlocks = blockCtResp.Ct
-
var recCtResp CountResp
-
if err := s.db.Raw("SELECT COUNT(*) AS ct FROM records WHERE did = ?", nil, urepo.Repo.Did).Scan(&recCtResp).Error; err != nil {
-
s.logger.Error("error getting record count", "error", err)
-
return helpers.ServerError(e, nil)
-
}
-
resp.IndexedRecords = recCtResp.Ct
-
var blobCtResp CountResp
-
if err := s.db.Raw("SELECT COUNT(*) AS ct FROM blobs WHERE did = ?", nil, urepo.Repo.Did).Scan(&blobCtResp).Error; err != nil {
-
s.logger.Error("error getting record count", "error", err)
return helpers.ServerError(e, nil)
}
resp.ExpectedBlobs = blobCtResp.Ct
return e.JSON(200, resp)
···
package server
import (
+
"errors"
+
"sync"
+
+
"github.com/bluesky-social/indigo/atproto/syntax"
"github.com/haileyok/cocoon/internal/helpers"
"github.com/haileyok/cocoon/models"
"github.com/ipfs/go-cid"
···
func (s *Server) handleServerCheckAccountStatus(e echo.Context) error {
urepo := e.Get("repo").(*models.RepoActor)
+
_, didErr := syntax.ParseDID(urepo.Repo.Did)
+
if didErr != nil {
+
s.logger.Error("error validating did", "err", didErr)
+
}
+
resp := ComAtprotoServerCheckAccountStatusResponse{
Activated: true, // TODO: should allow for deactivation etc.
+
ValidDid: didErr == nil,
RepoRev: urepo.Rev,
ImportedBlobs: 0, // TODO: ???
}
···
s.logger.Error("error casting cid", "error", err)
return helpers.ServerError(e, nil)
}
+
resp.RepoCommit = rootcid.String()
type CountResp struct {
···
}
var blockCtResp CountResp
+
var recCtResp CountResp
+
var blobCtResp CountResp
+
+
var wg sync.WaitGroup
+
var procErr error
+
+
wg.Add(1)
+
go func() {
+
defer wg.Done()
+
if err := s.db.Raw("SELECT COUNT(*) AS ct FROM blocks WHERE did = ?", nil, urepo.Repo.Did).Scan(&blockCtResp).Error; err != nil {
+
s.logger.Error("error getting block count", "error", err)
+
procErr = errors.Join(procErr, err)
+
}
+
}()
+
+
wg.Add(1)
+
go func() {
+
defer wg.Done()
+
if err := s.db.Raw("SELECT COUNT(*) AS ct FROM records WHERE did = ?", nil, urepo.Repo.Did).Scan(&recCtResp).Error; err != nil {
+
s.logger.Error("error getting record count", "error", err)
+
procErr = errors.Join(procErr, err)
+
}
+
}()
+
wg.Add(1)
+
go func() {
+
if err := s.db.Raw("SELECT COUNT(*) AS ct FROM blobs WHERE did = ?", nil, urepo.Repo.Did).Scan(&blobCtResp).Error; err != nil {
+
s.logger.Error("error getting expected blobs count", "error", err)
+
procErr = errors.Join(procErr, err)
+
}
+
}()
+
wg.Wait()
+
if procErr != nil {
return helpers.ServerError(e, nil)
}
+
+
resp.RepoBlocks = blockCtResp.Ct
+
resp.IndexedRecords = recCtResp.Ct
resp.ExpectedBlobs = blobCtResp.Ct
return e.JSON(200, resp)
+47 -46
server/handle_server_create_account.go
···
"github.com/Azure/go-autorest/autorest/to"
"github.com/bluesky-social/indigo/api/atproto"
-
"github.com/bluesky-social/indigo/atproto/crypto"
-
"github.com/bluesky-social/indigo/atproto/syntax"
"github.com/bluesky-social/indigo/events"
"github.com/bluesky-social/indigo/repo"
"github.com/bluesky-social/indigo/util"
···
func (s *Server) handleCreateAccount(e echo.Context) error {
var request ComAtprotoServerCreateAccountRequest
-
var signupDid string
-
customDidHeader := e.Request().Header.Get("authorization")
-
if customDidHeader != "" {
-
pts := strings.Split(customDidHeader, " ")
-
if len(pts) != 2 {
-
return helpers.InputError(e, to.StringPtr("InvalidDid"))
-
}
-
-
_, err := syntax.ParseDID(pts[1])
-
if err != nil {
-
return helpers.InputError(e, to.StringPtr("InvalidDid"))
-
}
-
-
signupDid = pts[1]
-
}
-
if err := e.Bind(&request); err != nil {
s.logger.Error("error receiving request", "endpoint", "com.atproto.server.createAccount", "error", err)
return helpers.ServerError(e, nil)
···
}
}
}
// see if the handle is already taken
-
_, err := s.getActorByHandle(request.Handle)
if err != nil && err != gorm.ErrRecordNotFound {
s.logger.Error("error looking up handle in db", "endpoint", "com.atproto.server.createAccount", "error", err)
return helpers.ServerError(e, nil)
}
-
if err == nil {
return helpers.InputError(e, to.StringPtr("HandleNotAvailable"))
}
-
if did, err := s.passport.ResolveHandle(e.Request().Context(), request.Handle); err == nil && did != "" {
return helpers.InputError(e, to.StringPtr("HandleNotAvailable"))
}
···
}
// see if the email is already taken
-
_, err = s.getRepoByEmail(request.Email)
if err != nil && err != gorm.ErrRecordNotFound {
s.logger.Error("error looking up email in db", "endpoint", "com.atproto.server.createAccount", "error", err)
return helpers.ServerError(e, nil)
}
-
if err == nil {
return helpers.InputError(e, to.StringPtr("EmailNotAvailable"))
}
// TODO: unsupported domains
-
k, err := crypto.GeneratePrivateKeyK256()
if err != nil {
s.logger.Error("error creating signing key", "endpoint", "com.atproto.server.createAccount", "error", err)
return helpers.ServerError(e, nil)
···
SigningKey: k.Bytes(),
}
-
actor := models.Actor{
-
Did: signupDid,
-
Handle: request.Handle,
-
}
-
if err := s.db.Create(&urepo, nil).Error; err != nil {
-
s.logger.Error("error inserting new repo", "error", err)
-
return helpers.ServerError(e, nil)
-
}
-
-
if err := s.db.Create(&actor, nil).Error; err != nil {
-
s.logger.Error("error inserting new actor", "error", err)
-
return helpers.ServerError(e, nil)
}
-
if customDidHeader == "" {
bs := s.getBlockstore(signupDid)
r := repo.NewRepo(context.TODO(), signupDid, bs)
···
s.logger.Error("error updating repo after commit", "error", err)
return helpers.ServerError(e, nil)
}
-
-
s.evtman.AddEvent(context.TODO(), &events.XRPCStreamEvent{
-
RepoHandle: &atproto.SyncSubscribeRepos_Handle{
-
Did: urepo.Did,
-
Handle: request.Handle,
-
Seq: time.Now().UnixMicro(), // TODO: no
-
Time: time.Now().Format(util.ISO8601),
-
},
-
})
s.evtman.AddEvent(context.TODO(), &events.XRPCStreamEvent{
RepoIdentity: &atproto.SyncSubscribeRepos_Identity{
···
"github.com/Azure/go-autorest/autorest/to"
"github.com/bluesky-social/indigo/api/atproto"
+
"github.com/bluesky-social/indigo/atproto/atcrypto"
"github.com/bluesky-social/indigo/events"
"github.com/bluesky-social/indigo/repo"
"github.com/bluesky-social/indigo/util"
···
func (s *Server) handleCreateAccount(e echo.Context) error {
var request ComAtprotoServerCreateAccountRequest
if err := e.Bind(&request); err != nil {
s.logger.Error("error receiving request", "endpoint", "com.atproto.server.createAccount", "error", err)
return helpers.ServerError(e, nil)
···
}
}
}
+
+
var signupDid string
+
if request.Did != nil {
+
signupDid = *request.Did;
+
+
token := strings.TrimSpace(strings.Replace(e.Request().Header.Get("authorization"), "Bearer ", "", 1))
+
if token == "" {
+
return helpers.UnauthorizedError(e, to.StringPtr("must authenticate to use an existing did"))
+
}
+
authDid, err := s.validateServiceAuth(e.Request().Context(), token, "com.atproto.server.createAccount")
+
+
if err != nil {
+
s.logger.Warn("error validating authorization token", "endpoint", "com.atproto.server.createAccount", "error", err)
+
return helpers.UnauthorizedError(e, to.StringPtr("invalid authorization token"))
+
}
+
+
if authDid != signupDid {
+
return helpers.ForbiddenError(e, to.StringPtr("auth did did not match signup did"))
+
}
+
}
// see if the handle is already taken
+
actor, err := s.getActorByHandle(request.Handle)
if err != nil && err != gorm.ErrRecordNotFound {
s.logger.Error("error looking up handle in db", "endpoint", "com.atproto.server.createAccount", "error", err)
return helpers.ServerError(e, nil)
}
+
if err == nil && actor.Did != signupDid {
return helpers.InputError(e, to.StringPtr("HandleNotAvailable"))
}
+
if did, err := s.passport.ResolveHandle(e.Request().Context(), request.Handle); err == nil && did != signupDid {
return helpers.InputError(e, to.StringPtr("HandleNotAvailable"))
}
···
}
// see if the email is already taken
+
existingRepo, err := s.getRepoByEmail(request.Email)
if err != nil && err != gorm.ErrRecordNotFound {
s.logger.Error("error looking up email in db", "endpoint", "com.atproto.server.createAccount", "error", err)
return helpers.ServerError(e, nil)
}
+
if err == nil && existingRepo.Did != signupDid {
return helpers.InputError(e, to.StringPtr("EmailNotAvailable"))
}
// TODO: unsupported domains
+
k, err := atcrypto.GeneratePrivateKeyK256()
if err != nil {
s.logger.Error("error creating signing key", "endpoint", "com.atproto.server.createAccount", "error", err)
return helpers.ServerError(e, nil)
···
SigningKey: k.Bytes(),
}
+
if actor == nil {
+
actor = &models.Actor{
+
Did: signupDid,
+
Handle: request.Handle,
+
}
+
if err := s.db.Create(&urepo, nil).Error; err != nil {
+
s.logger.Error("error inserting new repo", "error", err)
+
return helpers.ServerError(e, nil)
+
}
+
+
if err := s.db.Create(&actor, nil).Error; err != nil {
+
s.logger.Error("error inserting new actor", "error", err)
+
return helpers.ServerError(e, nil)
+
}
+
} else {
+
if err := s.db.Save(&actor, nil).Error; err != nil {
+
s.logger.Error("error inserting new actor", "error", err)
+
return helpers.ServerError(e, nil)
+
}
}
+
if request.Did == nil || *request.Did == "" {
bs := s.getBlockstore(signupDid)
r := repo.NewRepo(context.TODO(), signupDid, bs)
···
s.logger.Error("error updating repo after commit", "error", err)
return helpers.ServerError(e, nil)
}
s.evtman.AddEvent(context.TODO(), &events.XRPCStreamEvent{
RepoIdentity: &atproto.SyncSubscribeRepos_Identity{
+2 -2
server/handle_server_create_session.go
···
Email: repo.Email,
EmailConfirmed: repo.EmailConfirmedAt != nil,
EmailAuthFactor: false,
-
Active: true, // TODO: eventually do takedowns
-
Status: nil, // TODO eventually do takedowns
})
}
···
Email: repo.Email,
EmailConfirmed: repo.EmailConfirmedAt != nil,
EmailAuthFactor: false,
+
Active: repo.Active(),
+
Status: repo.Status(),
})
}
+46
server/handle_server_deactivate_account.go
···
···
+
package server
+
+
import (
+
"context"
+
"time"
+
+
"github.com/Azure/go-autorest/autorest/to"
+
"github.com/bluesky-social/indigo/api/atproto"
+
"github.com/bluesky-social/indigo/events"
+
"github.com/bluesky-social/indigo/util"
+
"github.com/haileyok/cocoon/internal/helpers"
+
"github.com/haileyok/cocoon/models"
+
"github.com/labstack/echo/v4"
+
)
+
+
type ComAtprotoServerDeactivateAccountRequest struct {
+
// NOTE: this implementation will not pay attention to this value
+
DeleteAfter time.Time `json:"deleteAfter"`
+
}
+
+
func (s *Server) handleServerDeactivateAccount(e echo.Context) error {
+
var req ComAtprotoServerDeactivateAccountRequest
+
if err := e.Bind(&req); err != nil {
+
s.logger.Error("error binding", "error", err)
+
return helpers.ServerError(e, nil)
+
}
+
+
urepo := e.Get("repo").(*models.RepoActor)
+
+
if err := s.db.Exec("UPDATE repos SET deactivated = ? WHERE did = ?", nil, true, urepo.Repo.Did).Error; err != nil {
+
s.logger.Error("error updating account status to deactivated", "error", err)
+
return helpers.ServerError(e, nil)
+
}
+
+
s.evtman.AddEvent(context.TODO(), &events.XRPCStreamEvent{
+
RepoAccount: &atproto.SyncSubscribeRepos_Account{
+
Active: false,
+
Did: urepo.Repo.Did,
+
Status: to.StringPtr("deactivated"),
+
Seq: time.Now().UnixMicro(), // TODO: bad puppy
+
Time: time.Now().Format(util.ISO8601),
+
},
+
})
+
+
return e.NoContent(200)
+
}
+2 -2
server/handle_server_get_session.go
···
Email: repo.Email,
EmailConfirmed: repo.EmailConfirmedAt != nil,
EmailAuthFactor: false, // TODO: todo todo
-
Active: true,
-
Status: nil,
})
}
···
Email: repo.Email,
EmailConfirmed: repo.EmailConfirmedAt != nil,
EmailAuthFactor: false, // TODO: todo todo
+
Active: repo.Active(),
+
Status: repo.Status(),
})
}
+2 -2
server/handle_server_refresh_session.go
···
RefreshJwt: sess.RefreshToken,
Handle: repo.Handle,
Did: repo.Repo.Did,
-
Active: true,
-
Status: nil,
})
}
···
RefreshJwt: sess.RefreshToken,
Handle: repo.Handle,
Did: repo.Repo.Did,
+
Active: repo.Active(),
+
Status: repo.Status(),
})
}
+84 -8
server/handle_sync_get_blob.go
···
import (
"bytes"
"github.com/haileyok/cocoon/internal/helpers"
"github.com/haileyok/cocoon/models"
"github.com/ipfs/go-cid"
···
return helpers.InputError(e, nil)
}
var blob models.Blob
if err := s.db.Raw("SELECT * FROM blobs WHERE did = ? AND cid = ?", nil, did, c.Bytes()).Scan(&blob).Error; err != nil {
s.logger.Error("error looking up blob", "error", err)
···
buf := new(bytes.Buffer)
-
var parts []models.BlobPart
-
if err := s.db.Raw("SELECT * FROM blob_parts WHERE blob_id = ? ORDER BY idx", nil, blob.ID).Scan(&parts).Error; err != nil {
-
s.logger.Error("error getting blob parts", "error", err)
-
return helpers.ServerError(e, nil)
-
}
-
// TODO: we can just stream this, don't need to make a buffer
-
for _, p := range parts {
-
buf.Write(p.Data)
}
e.Response().Header().Set(echo.HeaderContentDisposition, "attachment; filename="+c.String())
···
import (
"bytes"
+
"fmt"
+
"io"
+
"github.com/Azure/go-autorest/autorest/to"
+
"github.com/aws/aws-sdk-go/aws"
+
"github.com/aws/aws-sdk-go/aws/credentials"
+
"github.com/aws/aws-sdk-go/aws/session"
+
"github.com/aws/aws-sdk-go/service/s3"
"github.com/haileyok/cocoon/internal/helpers"
"github.com/haileyok/cocoon/models"
"github.com/ipfs/go-cid"
···
return helpers.InputError(e, nil)
}
+
urepo, err := s.getRepoActorByDid(did)
+
if err != nil {
+
s.logger.Error("could not find user for requested blob", "error", err)
+
return helpers.InputError(e, nil)
+
}
+
+
status := urepo.Status()
+
if status != nil {
+
if *status == "deactivated" {
+
return helpers.InputError(e, to.StringPtr("RepoDeactivated"))
+
}
+
}
+
var blob models.Blob
if err := s.db.Raw("SELECT * FROM blobs WHERE did = ? AND cid = ?", nil, did, c.Bytes()).Scan(&blob).Error; err != nil {
s.logger.Error("error looking up blob", "error", err)
···
buf := new(bytes.Buffer)
+
if blob.Storage == "sqlite" {
+
var parts []models.BlobPart
+
if err := s.db.Raw("SELECT * FROM blob_parts WHERE blob_id = ? ORDER BY idx", nil, blob.ID).Scan(&parts).Error; err != nil {
+
s.logger.Error("error getting blob parts", "error", err)
+
return helpers.ServerError(e, nil)
+
}
+
+
// TODO: we can just stream this, don't need to make a buffer
+
for _, p := range parts {
+
buf.Write(p.Data)
+
}
+
} else if blob.Storage == "s3" {
+
if !(s.s3Config != nil && s.s3Config.BlobstoreEnabled) {
+
s.logger.Error("s3 storage disabled")
+
return helpers.ServerError(e, nil)
+
}
+
config := &aws.Config{
+
Region: aws.String(s.s3Config.Region),
+
Credentials: credentials.NewStaticCredentials(s.s3Config.AccessKey, s.s3Config.SecretKey, ""),
+
}
+
+
if s.s3Config.Endpoint != "" {
+
config.Endpoint = aws.String(s.s3Config.Endpoint)
+
config.S3ForcePathStyle = aws.Bool(true)
+
}
+
+
sess, err := session.NewSession(config)
+
if err != nil {
+
s.logger.Error("error creating aws session", "error", err)
+
return helpers.ServerError(e, nil)
+
}
+
+
svc := s3.New(sess)
+
if result, err := svc.GetObject(&s3.GetObjectInput{
+
Bucket: aws.String(s.s3Config.Bucket),
+
Key: aws.String(fmt.Sprintf("blobs/%s/%s", urepo.Repo.Did, c.String())),
+
}); err != nil {
+
s.logger.Error("error getting blob from s3", "error", err)
+
return helpers.ServerError(e, nil)
+
} else {
+
read := 0
+
part := 0
+
partBuf := make([]byte, 0x10000)
+
+
for {
+
n, err := io.ReadFull(result.Body, partBuf)
+
if err == io.ErrUnexpectedEOF || err == io.EOF {
+
if n == 0 {
+
break
+
}
+
} else if err != nil && err != io.ErrUnexpectedEOF {
+
s.logger.Error("error reading blob", "error", err)
+
return helpers.ServerError(e, nil)
+
}
+
+
data := partBuf[:n]
+
read += n
+
buf.Write(data)
+
part++
+
}
+
}
+
} else {
+
s.logger.Error("unknown storage", "storage", blob.Storage)
+
return helpers.ServerError(e, nil)
}
e.Response().Header().Set(echo.HeaderContentDisposition, "attachment; filename="+c.String())
+10 -8
server/handle_sync_get_blocks.go
···
"github.com/labstack/echo/v4"
)
func (s *Server) handleGetBlocks(e echo.Context) error {
ctx := e.Request().Context()
-
did := e.QueryParam("did")
-
if did == "" {
-
return helpers.InputError(e, nil)
-
}
-
cidstrs, ok := e.QueryParams()["cids"]
-
if !ok {
return helpers.InputError(e, nil)
}
var cids []cid.Cid
-
for _, cs := range cidstrs {
c, err := cid.Cast([]byte(cs))
if err != nil {
return err
···
cids = append(cids, c)
}
-
urepo, err := s.getRepoActorByDid(did)
if err != nil {
return helpers.ServerError(e, nil)
}
···
"github.com/labstack/echo/v4"
)
+
type ComAtprotoSyncGetBlocksRequest struct {
+
Did string `query:"did"`
+
Cids []string `query:"cids"`
+
}
+
func (s *Server) handleGetBlocks(e echo.Context) error {
ctx := e.Request().Context()
+
var req ComAtprotoSyncGetBlocksRequest
+
if err := e.Bind(&req); err != nil {
return helpers.InputError(e, nil)
}
+
var cids []cid.Cid
+
for _, cs := range req.Cids {
c, err := cid.Cast([]byte(cs))
if err != nil {
return err
···
cids = append(cids, c)
}
+
urepo, err := s.getRepoActorByDid(req.Did)
if err != nil {
return helpers.ServerError(e, nil)
}
+3 -1
server/handle_sync_get_record.go
···
)
func (s *Server) handleSyncGetRecord(e echo.Context) error {
did := e.QueryParam("did")
collection := e.QueryParam("collection")
rkey := e.QueryParam("rkey")
···
return helpers.ServerError(e, nil)
}
-
root, blocks, err := s.repoman.getRecordProof(urepo, collection, rkey)
if err != nil {
return err
}
···
)
func (s *Server) handleSyncGetRecord(e echo.Context) error {
+
ctx := e.Request().Context()
+
did := e.QueryParam("did")
collection := e.QueryParam("collection")
rkey := e.QueryParam("rkey")
···
return helpers.ServerError(e, nil)
}
+
root, blocks, err := s.repoman.getRecordProof(ctx, urepo, collection, rkey)
if err != nil {
return err
}
+2 -2
server/handle_sync_get_repo_status.go
···
return e.JSON(200, ComAtprotoSyncGetRepoStatusResponse{
Did: urepo.Repo.Did,
-
Active: true,
-
Status: nil,
Rev: &urepo.Rev,
})
}
···
return e.JSON(200, ComAtprotoSyncGetRepoStatusResponse{
Did: urepo.Repo.Did,
+
Active: urepo.Active(),
+
Status: urepo.Status(),
Rev: &urepo.Rev,
})
}
+14
server/handle_sync_list_blobs.go
···
package server
import (
"github.com/haileyok/cocoon/internal/helpers"
"github.com/haileyok/cocoon/models"
"github.com/ipfs/go-cid"
···
cursorquery = "AND created_at < ?"
}
params = append(params, limit)
var blobs []models.Blob
if err := s.db.Raw("SELECT * FROM blobs WHERE did = ? "+cursorquery+" ORDER BY created_at DESC LIMIT ?", nil, params...).Scan(&blobs).Error; err != nil {
···
package server
import (
+
"github.com/Azure/go-autorest/autorest/to"
"github.com/haileyok/cocoon/internal/helpers"
"github.com/haileyok/cocoon/models"
"github.com/ipfs/go-cid"
···
cursorquery = "AND created_at < ?"
}
params = append(params, limit)
+
+
urepo, err := s.getRepoActorByDid(did)
+
if err != nil {
+
s.logger.Error("could not find user for requested blobs", "error", err)
+
return helpers.InputError(e, nil)
+
}
+
+
status := urepo.Status()
+
if status != nil {
+
if *status == "deactivated" {
+
return helpers.InputError(e, to.StringPtr("RepoDeactivated"))
+
}
+
}
var blobs []models.Blob
if err := s.db.Raw("SELECT * FROM blobs WHERE did = ? "+cursorquery+" ORDER BY created_at DESC LIMIT ?", nil, params...).Scan(&blobs).Error; err != nil {
+31 -29
server/handle_sync_subscribe_repos.go
···
package server
import (
-
"fmt"
-
"net/http"
"github.com/bluesky-social/indigo/events"
"github.com/bluesky-social/indigo/lex/util"
···
"github.com/labstack/echo/v4"
)
-
var upgrader = websocket.Upgrader{
-
ReadBufferSize: 1024,
-
WriteBufferSize: 1024,
-
CheckOrigin: func(r *http.Request) bool {
-
return true
-
},
-
}
-
func (s *Server) handleSyncSubscribeRepos(e echo.Context) error {
conn, err := websocket.Upgrade(e.Response().Writer, e.Request(), e.Response().Header(), 1<<10, 1<<10)
if err != nil {
return err
}
-
s.logger.Info("new connection", "ua", e.Request().UserAgent())
-
-
ctx := e.Request().Context()
-
ident := e.RealIP() + "-" + e.Request().UserAgent()
evts, cancel, err := s.evtman.Subscribe(ctx, ident, func(evt *events.XRPCStreamEvent) bool {
return true
···
for evt := range evts {
wc, err := conn.NextWriter(websocket.BinaryMessage)
if err != nil {
-
return err
}
-
var obj util.CBOR
switch {
case evt.Error != nil:
header.Op = events.EvtKindErrorFrame
···
case evt.RepoCommit != nil:
header.MsgType = "#commit"
obj = evt.RepoCommit
-
case evt.RepoHandle != nil:
-
header.MsgType = "#handle"
-
obj = evt.RepoHandle
case evt.RepoIdentity != nil:
header.MsgType = "#identity"
obj = evt.RepoIdentity
···
case evt.RepoInfo != nil:
header.MsgType = "#info"
obj = evt.RepoInfo
-
case evt.RepoMigrate != nil:
-
header.MsgType = "#migrate"
-
obj = evt.RepoMigrate
-
case evt.RepoTombstone != nil:
-
header.MsgType = "#tombstone"
-
obj = evt.RepoTombstone
default:
-
return fmt.Errorf("unrecognized event kind")
}
if err := header.MarshalCBOR(wc); err != nil {
-
return fmt.Errorf("failed to write header: %w", err)
}
if err := obj.MarshalCBOR(wc); err != nil {
-
return fmt.Errorf("failed to write event: %w", err)
}
if err := wc.Close(); err != nil {
-
return fmt.Errorf("failed to flush-close our event write: %w", err)
}
}
return nil
···
package server
import (
+
"context"
+
"time"
"github.com/bluesky-social/indigo/events"
"github.com/bluesky-social/indigo/lex/util"
···
"github.com/labstack/echo/v4"
)
+
func (s *Server) handleSyncSubscribeRepos(e echo.Context) error {
+
ctx := e.Request().Context()
+
logger := s.logger.With("component", "subscribe-repos-websocket")
conn, err := websocket.Upgrade(e.Response().Writer, e.Request(), e.Response().Header(), 1<<10, 1<<10)
if err != nil {
+
logger.Error("unable to establish websocket with relay", "err", err)
return err
}
ident := e.RealIP() + "-" + e.Request().UserAgent()
+
logger = logger.With("ident", ident)
+
logger.Info("new connection established")
evts, cancel, err := s.evtman.Subscribe(ctx, ident, func(evt *events.XRPCStreamEvent) bool {
return true
···
for evt := range evts {
wc, err := conn.NextWriter(websocket.BinaryMessage)
if err != nil {
+
logger.Error("error writing message to relay", "err", err)
+
break
}
+
if ctx.Err() != nil {
+
logger.Error("context error", "err", err)
+
break
+
}
+
var obj util.CBOR
switch {
case evt.Error != nil:
header.Op = events.EvtKindErrorFrame
···
case evt.RepoCommit != nil:
header.MsgType = "#commit"
obj = evt.RepoCommit
case evt.RepoIdentity != nil:
header.MsgType = "#identity"
obj = evt.RepoIdentity
···
case evt.RepoInfo != nil:
header.MsgType = "#info"
obj = evt.RepoInfo
default:
+
logger.Warn("unrecognized event kind")
+
return nil
}
if err := header.MarshalCBOR(wc); err != nil {
+
logger.Error("failed to write header to relay", "err", err)
+
break
}
if err := obj.MarshalCBOR(wc); err != nil {
+
logger.Error("failed to write event to relay", "err", err)
+
break
}
if err := wc.Close(); err != nil {
+
logger.Error("failed to flush-close our event write", "err", err)
+
break
}
+
}
+
+
// we should tell the relay to request a new crawl at this point if we got disconnected
+
// use a new context since the old one might be cancelled at this point
+
ctx, cancel = context.WithTimeout(context.Background(), 10*time.Second)
+
defer cancel()
+
if err := s.requestCrawl(ctx); err != nil {
+
logger.Error("error requesting crawls", "err", err)
}
return nil
+19
server/mail.go
···
return nil
}
func (s *Server) sendEmailUpdate(email, handle, code string) error {
if s.mail == nil {
return nil
···
return nil
}
+
func (s *Server) sendPlcTokenReset(email, handle, code string) error {
+
if s.mail == nil {
+
return nil
+
}
+
+
s.mailLk.Lock()
+
defer s.mailLk.Unlock()
+
+
s.mail.To(email)
+
s.mail.Subject("PLC token for " + s.config.Hostname)
+
s.mail.Plain().Set(fmt.Sprintf("Hello %s. Your PLC operation code is %s. This code will expire in ten minutes.", handle, code))
+
+
if err := s.mail.Send(); err != nil {
+
return err
+
}
+
+
return nil
+
}
+
func (s *Server) sendEmailUpdate(email, handle, code string) error {
if s.mail == nil {
return nil
+8 -1
server/middleware.go
···
import (
"crypto/sha256"
"encoding/base64"
"fmt"
"strings"
"time"
···
"github.com/golang-jwt/jwt/v4"
"github.com/haileyok/cocoon/internal/helpers"
"github.com/haileyok/cocoon/models"
"github.com/haileyok/cocoon/oauth/provider"
"github.com/labstack/echo/v4"
"gitlab.com/yawning/secp256k1-voi"
···
proof, err := s.oauthProvider.DpopManager.CheckProof(e.Request().Method, "https://"+s.config.Hostname+e.Request().URL.String(), e.Request().Header, to.StringPtr(accessToken))
if err != nil {
s.logger.Error("invalid dpop proof", "error", err)
-
return helpers.InputError(e, to.StringPtr(err.Error()))
}
var oauthToken provider.OauthToken
···
import (
"crypto/sha256"
"encoding/base64"
+
"errors"
"fmt"
"strings"
"time"
···
"github.com/golang-jwt/jwt/v4"
"github.com/haileyok/cocoon/internal/helpers"
"github.com/haileyok/cocoon/models"
+
"github.com/haileyok/cocoon/oauth/dpop"
"github.com/haileyok/cocoon/oauth/provider"
"github.com/labstack/echo/v4"
"gitlab.com/yawning/secp256k1-voi"
···
proof, err := s.oauthProvider.DpopManager.CheckProof(e.Request().Method, "https://"+s.config.Hostname+e.Request().URL.String(), e.Request().Header, to.StringPtr(accessToken))
if err != nil {
+
if errors.Is(err, dpop.ErrUseDpopNonce) {
+
return e.JSON(400, map[string]string{
+
"error": "use_dpop_nonce",
+
})
+
}
s.logger.Error("invalid dpop proof", "error", err)
+
return helpers.InputError(e, nil)
}
var oauthToken provider.OauthToken
+32 -23
server/repo.go
···
"github.com/Azure/go-autorest/autorest/to"
"github.com/bluesky-social/indigo/api/atproto"
-
"github.com/bluesky-social/indigo/atproto/data"
"github.com/bluesky-social/indigo/atproto/syntax"
"github.com/bluesky-social/indigo/carstore"
"github.com/bluesky-social/indigo/events"
···
}
func (mm *MarshalableMap) MarshalCBOR(w io.Writer) error {
-
data, err := data.MarshalCBOR(*mm)
if err != nil {
return err
}
···
}
// TODO make use of swap commit
-
func (rm *RepoMan) applyWrites(urepo models.Repo, writes []Op, swapCommit *string) ([]ApplyWriteResult, error) {
rootcid, err := cid.Cast(urepo.Root)
if err != nil {
return nil, err
···
dbs := rm.s.getBlockstore(urepo.Did)
bs := recording_blockstore.New(dbs)
-
r, err := repo.OpenRepo(context.TODO(), dbs, rootcid)
-
entries := []models.Record{}
-
var results []ApplyWriteResult
for i, op := range writes {
if op.Type != OpTypeCreate && op.Rkey == nil {
return nil, fmt.Errorf("invalid rkey")
} else if op.Type == OpTypeCreate && op.Rkey != nil {
-
_, _, err := r.GetRecord(context.TODO(), op.Collection+"/"+*op.Rkey)
if err == nil {
op.Type = OpTypeUpdate
}
···
if err != nil {
return nil, err
}
-
out, err := data.UnmarshalJSON(j)
if err != nil {
return nil, err
}
mm := MarshalableMap(out)
-
nc, err := r.PutRecord(context.TODO(), op.Collection+"/"+*op.Rkey, &mm)
if err != nil {
return nil, err
}
-
d, err := data.MarshalCBOR(mm)
if err != nil {
return nil, err
}
···
Rkey: *op.Rkey,
Value: old.Value,
})
-
err := r.DeleteRecord(context.TODO(), op.Collection+"/"+*op.Rkey)
if err != nil {
return nil, err
}
···
if err != nil {
return nil, err
}
-
out, err := data.UnmarshalJSON(j)
if err != nil {
return nil, err
}
mm := MarshalableMap(out)
-
nc, err := r.UpdateRecord(context.TODO(), op.Collection+"/"+*op.Rkey, &mm)
if err != nil {
return nil, err
}
-
d, err := data.MarshalCBOR(mm)
if err != nil {
return nil, err
}
···
}
}
-
newroot, rev, err := r.Commit(context.TODO(), urepo.SignFor)
if err != nil {
return nil, err
}
···
Roots: []cid.Cid{newroot},
Version: 1,
})
if _, err := carstore.LdWrite(buf, hb); err != nil {
return nil, err
}
-
diffops, err := r.DiffSince(context.TODO(), rootcid)
if err != nil {
return nil, err
}
···
})
}
-
blk, err := dbs.Get(context.TODO(), c)
if err != nil {
return nil, err
}
···
}
}
-
rm.s.evtman.AddEvent(context.TODO(), &events.XRPCStreamEvent{
RepoCommit: &atproto.SyncSubscribeRepos_Commit{
Repo: urepo.Did,
Blocks: buf.Bytes(),
···
},
})
-
if err := rm.s.UpdateRepo(context.TODO(), urepo.Did, newroot, rev); err != nil {
return nil, err
}
···
return results, nil
}
-
func (rm *RepoMan) getRecordProof(urepo models.Repo, collection, rkey string) (cid.Cid, []blocks.Block, error) {
c, err := cid.Cast(urepo.Root)
if err != nil {
return cid.Undef, nil, err
···
dbs := rm.s.getBlockstore(urepo.Did)
bs := recording_blockstore.New(dbs)
-
r, err := repo.OpenRepo(context.TODO(), bs, c)
if err != nil {
return cid.Undef, nil, err
}
-
_, _, err = r.GetRecordBytes(context.TODO(), collection+"/"+rkey)
if err != nil {
return cid.Undef, nil, err
}
···
func getBlobCidsFromCbor(cbor []byte) ([]cid.Cid, error) {
var cids []cid.Cid
-
decoded, err := data.UnmarshalCBOR(cbor)
if err != nil {
return nil, fmt.Errorf("error unmarshaling cbor: %w", err)
}
···
"github.com/Azure/go-autorest/autorest/to"
"github.com/bluesky-social/indigo/api/atproto"
+
"github.com/bluesky-social/indigo/atproto/atdata"
"github.com/bluesky-social/indigo/atproto/syntax"
"github.com/bluesky-social/indigo/carstore"
"github.com/bluesky-social/indigo/events"
···
}
func (mm *MarshalableMap) MarshalCBOR(w io.Writer) error {
+
data, err := atdata.MarshalCBOR(*mm)
if err != nil {
return err
}
···
}
// TODO make use of swap commit
+
func (rm *RepoMan) applyWrites(ctx context.Context, urepo models.Repo, writes []Op, swapCommit *string) ([]ApplyWriteResult, error) {
rootcid, err := cid.Cast(urepo.Root)
if err != nil {
return nil, err
···
dbs := rm.s.getBlockstore(urepo.Did)
bs := recording_blockstore.New(dbs)
+
r, err := repo.OpenRepo(ctx, bs, rootcid)
+
entries := make([]models.Record, 0, len(writes))
+
results := make([]ApplyWriteResult, 0, len(writes))
for i, op := range writes {
if op.Type != OpTypeCreate && op.Rkey == nil {
return nil, fmt.Errorf("invalid rkey")
} else if op.Type == OpTypeCreate && op.Rkey != nil {
+
_, _, err := r.GetRecord(ctx, op.Collection+"/"+*op.Rkey)
if err == nil {
op.Type = OpTypeUpdate
}
···
if err != nil {
return nil, err
}
+
out, err := atdata.UnmarshalJSON(j)
if err != nil {
return nil, err
}
mm := MarshalableMap(out)
+
+
// HACK: if a record doesn't contain a $type, we can manually set it here based on the op's collection
+
if mm["$type"] == "" {
+
mm["$type"] = op.Collection
+
}
+
+
nc, err := r.PutRecord(ctx, op.Collection+"/"+*op.Rkey, &mm)
if err != nil {
return nil, err
}
+
d, err := atdata.MarshalCBOR(mm)
if err != nil {
return nil, err
}
···
Rkey: *op.Rkey,
Value: old.Value,
})
+
err := r.DeleteRecord(ctx, op.Collection+"/"+*op.Rkey)
if err != nil {
return nil, err
}
···
if err != nil {
return nil, err
}
+
out, err := atdata.UnmarshalJSON(j)
if err != nil {
return nil, err
}
mm := MarshalableMap(out)
+
nc, err := r.UpdateRecord(ctx, op.Collection+"/"+*op.Rkey, &mm)
if err != nil {
return nil, err
}
+
d, err := atdata.MarshalCBOR(mm)
if err != nil {
return nil, err
}
···
}
}
+
newroot, rev, err := r.Commit(ctx, urepo.SignFor)
if err != nil {
return nil, err
}
···
Roots: []cid.Cid{newroot},
Version: 1,
})
+
if err != nil {
+
return nil, err
+
}
if _, err := carstore.LdWrite(buf, hb); err != nil {
return nil, err
}
+
diffops, err := r.DiffSince(ctx, rootcid)
if err != nil {
return nil, err
}
···
})
}
+
blk, err := dbs.Get(ctx, c)
if err != nil {
return nil, err
}
···
}
}
+
rm.s.evtman.AddEvent(ctx, &events.XRPCStreamEvent{
RepoCommit: &atproto.SyncSubscribeRepos_Commit{
Repo: urepo.Did,
Blocks: buf.Bytes(),
···
},
})
+
if err := rm.s.UpdateRepo(ctx, urepo.Did, newroot, rev); err != nil {
return nil, err
}
···
return results, nil
}
+
func (rm *RepoMan) getRecordProof(ctx context.Context, urepo models.Repo, collection, rkey string) (cid.Cid, []blocks.Block, error) {
c, err := cid.Cast(urepo.Root)
if err != nil {
return cid.Undef, nil, err
···
dbs := rm.s.getBlockstore(urepo.Did)
bs := recording_blockstore.New(dbs)
+
r, err := repo.OpenRepo(ctx, bs, c)
if err != nil {
return cid.Undef, nil, err
}
+
_, _, err = r.GetRecordBytes(ctx, collection+"/"+rkey)
if err != nil {
return cid.Undef, nil, err
}
···
func getBlobCidsFromCbor(cbor []byte) ([]cid.Cid, error) {
var cids []cid.Cid
+
decoded, err := atdata.UnmarshalCBOR(cbor)
if err != nil {
return nil, fmt.Errorf("error unmarshaling cbor: %w", err)
}
+76 -38
server/server.go
···
)
type S3Config struct {
-
BackupsEnabled bool
-
Endpoint string
-
Region string
-
Bucket string
-
AccessKey string
-
SecretKey string
}
type Server struct {
···
oauthProvider *provider.Provider
evtman *events.EventManager
passport *identity.Passport
dbName string
s3Config *S3Config
···
SessionSecret string
-
DefaultAtprotoProxy string
-
BlockstoreVariant BlockstoreVariant
}
type config struct {
-
Version string
-
Did string
-
Hostname string
-
ContactEmail string
-
EnforcePeering bool
-
Relays []string
-
AdminPassword string
-
SmtpEmail string
-
SmtpName string
-
DefaultAtprotoProxy string
-
BlockstoreVariant BlockstoreVariant
}
type CustomValidator struct {
···
IdleTimeout: 5 * time.Minute,
}
-
gdb, err := gorm.Open(sqlite.Open("cocoon.db"), &gorm.Config{})
if err != nil {
return nil, err
}
···
plcClient: plcClient,
privateKey: &pkey,
config: &config{
-
Version: args.Version,
-
Did: args.Did,
-
Hostname: args.Hostname,
-
ContactEmail: args.ContactEmail,
-
EnforcePeering: false,
-
Relays: args.Relays,
-
AdminPassword: args.AdminPassword,
-
SmtpName: args.SmtpName,
-
SmtpEmail: args.SmtpEmail,
-
DefaultAtprotoProxy: args.DefaultAtprotoProxy,
-
BlockstoreVariant: args.BlockstoreVariant,
},
evtman: events.NewEventManager(events.NewMemPersister()),
passport: identity.NewPassport(h, identity.NewMemCache(10_000)),
···
// TODO: should validate these args
if args.SmtpUser == "" || args.SmtpPass == "" || args.SmtpHost == "" || args.SmtpPort == "" || args.SmtpEmail == "" || args.SmtpName == "" {
-
args.Logger.Warn("not enough smpt args were provided. mailing will not work for your server.")
} else {
mail := mailyak.New(args.SmtpHost+":"+args.SmtpPort, smtp.PlainAuth("", args.SmtpUser, args.SmtpPass, args.SmtpHost))
mail.From(s.config.SmtpEmail)
···
// public
s.echo.GET("/xrpc/com.atproto.identity.resolveHandle", s.handleResolveHandle)
-
s.echo.POST("/xrpc/com.atproto.server.createAccount", s.handleCreateAccount)
s.echo.POST("/xrpc/com.atproto.server.createAccount", s.handleCreateAccount)
s.echo.POST("/xrpc/com.atproto.server.createSession", s.handleCreateSession)
s.echo.GET("/xrpc/com.atproto.server.describeServer", s.handleDescribeServer)
···
s.echo.GET("/xrpc/com.atproto.server.getSession", s.handleGetSession, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware)
s.echo.POST("/xrpc/com.atproto.server.refreshSession", s.handleRefreshSession, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware)
s.echo.POST("/xrpc/com.atproto.server.deleteSession", s.handleDeleteSession, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware)
s.echo.POST("/xrpc/com.atproto.identity.updateHandle", s.handleIdentityUpdateHandle, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware)
s.echo.POST("/xrpc/com.atproto.server.confirmEmail", s.handleServerConfirmEmail, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware)
s.echo.POST("/xrpc/com.atproto.server.requestEmailConfirmation", s.handleServerRequestEmailConfirmation, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware)
s.echo.POST("/xrpc/com.atproto.server.requestPasswordReset", s.handleServerRequestPasswordReset) // AUTH NOT REQUIRED FOR THIS ONE
···
s.echo.POST("/xrpc/com.atproto.server.updateEmail", s.handleServerUpdateEmail, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware)
s.echo.GET("/xrpc/com.atproto.server.getServiceAuth", s.handleServerGetServiceAuth, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware)
s.echo.GET("/xrpc/com.atproto.server.checkAccountStatus", s.handleServerCheckAccountStatus, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware)
// repo
s.echo.POST("/xrpc/com.atproto.repo.createRecord", s.handleCreateRecord, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware)
···
// stupid silly endpoints
s.echo.GET("/xrpc/app.bsky.actor.getPreferences", s.handleActorGetPreferences, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware)
s.echo.POST("/xrpc/app.bsky.actor.putPreferences", s.handleActorPutPreferences, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware)
// admin routes
s.echo.POST("/xrpc/com.atproto.server.createInviteCode", s.handleCreateInviteCode, s.handleAdminMiddleware)
···
go s.backupRoutine()
for _, relay := range s.config.Relays {
cli := xrpc.Client{Host: relay}
-
atproto.SyncRequestCrawl(ctx, &cli, &atproto.SyncRequestCrawl_Input{
Hostname: s.config.Hostname,
-
})
}
-
<-ctx.Done()
-
-
fmt.Println("shut down")
return nil
}
···
)
type S3Config struct {
+
BackupsEnabled bool
+
BlobstoreEnabled bool
+
Endpoint string
+
Region string
+
Bucket string
+
AccessKey string
+
SecretKey string
}
type Server struct {
···
oauthProvider *provider.Provider
evtman *events.EventManager
passport *identity.Passport
+
fallbackProxy string
+
+
lastRequestCrawl time.Time
+
requestCrawlMu sync.Mutex
dbName string
s3Config *S3Config
···
SessionSecret string
BlockstoreVariant BlockstoreVariant
+
FallbackProxy string
}
type config struct {
+
Version string
+
Did string
+
Hostname string
+
ContactEmail string
+
EnforcePeering bool
+
Relays []string
+
AdminPassword string
+
SmtpEmail string
+
SmtpName string
+
BlockstoreVariant BlockstoreVariant
+
FallbackProxy string
}
type CustomValidator struct {
···
IdleTimeout: 5 * time.Minute,
}
+
gdb, err := gorm.Open(sqlite.Open(args.DbName), &gorm.Config{})
if err != nil {
return nil, err
}
···
plcClient: plcClient,
privateKey: &pkey,
config: &config{
+
Version: args.Version,
+
Did: args.Did,
+
Hostname: args.Hostname,
+
ContactEmail: args.ContactEmail,
+
EnforcePeering: false,
+
Relays: args.Relays,
+
AdminPassword: args.AdminPassword,
+
SmtpName: args.SmtpName,
+
SmtpEmail: args.SmtpEmail,
+
BlockstoreVariant: args.BlockstoreVariant,
+
FallbackProxy: args.FallbackProxy,
},
evtman: events.NewEventManager(events.NewMemPersister()),
passport: identity.NewPassport(h, identity.NewMemCache(10_000)),
···
// TODO: should validate these args
if args.SmtpUser == "" || args.SmtpPass == "" || args.SmtpHost == "" || args.SmtpPort == "" || args.SmtpEmail == "" || args.SmtpName == "" {
+
args.Logger.Warn("not enough smtp args were provided. mailing will not work for your server.")
} else {
mail := mailyak.New(args.SmtpHost+":"+args.SmtpPort, smtp.PlainAuth("", args.SmtpUser, args.SmtpPass, args.SmtpHost))
mail.From(s.config.SmtpEmail)
···
// public
s.echo.GET("/xrpc/com.atproto.identity.resolveHandle", s.handleResolveHandle)
s.echo.POST("/xrpc/com.atproto.server.createAccount", s.handleCreateAccount)
s.echo.POST("/xrpc/com.atproto.server.createSession", s.handleCreateSession)
s.echo.GET("/xrpc/com.atproto.server.describeServer", s.handleDescribeServer)
···
s.echo.GET("/xrpc/com.atproto.server.getSession", s.handleGetSession, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware)
s.echo.POST("/xrpc/com.atproto.server.refreshSession", s.handleRefreshSession, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware)
s.echo.POST("/xrpc/com.atproto.server.deleteSession", s.handleDeleteSession, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware)
+
s.echo.GET("/xrpc/com.atproto.identity.getRecommendedDidCredentials", s.handleGetRecommendedDidCredentials, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware)
s.echo.POST("/xrpc/com.atproto.identity.updateHandle", s.handleIdentityUpdateHandle, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware)
+
s.echo.POST("/xrpc/com.atproto.identity.requestPlcOperationSignature", s.handleIdentityRequestPlcOperationSignature, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware)
+
s.echo.POST("/xrpc/com.atproto.identity.signPlcOperation", s.handleSignPlcOperation, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware)
+
s.echo.POST("/xrpc/com.atproto.identity.submitPlcOperation", s.handleSubmitPlcOperation, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware)
s.echo.POST("/xrpc/com.atproto.server.confirmEmail", s.handleServerConfirmEmail, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware)
s.echo.POST("/xrpc/com.atproto.server.requestEmailConfirmation", s.handleServerRequestEmailConfirmation, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware)
s.echo.POST("/xrpc/com.atproto.server.requestPasswordReset", s.handleServerRequestPasswordReset) // AUTH NOT REQUIRED FOR THIS ONE
···
s.echo.POST("/xrpc/com.atproto.server.updateEmail", s.handleServerUpdateEmail, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware)
s.echo.GET("/xrpc/com.atproto.server.getServiceAuth", s.handleServerGetServiceAuth, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware)
s.echo.GET("/xrpc/com.atproto.server.checkAccountStatus", s.handleServerCheckAccountStatus, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware)
+
s.echo.POST("/xrpc/com.atproto.server.deactivateAccount", s.handleServerDeactivateAccount, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware)
+
s.echo.POST("/xrpc/com.atproto.server.activateAccount", s.handleServerActivateAccount, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware)
// repo
s.echo.POST("/xrpc/com.atproto.repo.createRecord", s.handleCreateRecord, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware)
···
// stupid silly endpoints
s.echo.GET("/xrpc/app.bsky.actor.getPreferences", s.handleActorGetPreferences, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware)
s.echo.POST("/xrpc/app.bsky.actor.putPreferences", s.handleActorPutPreferences, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware)
+
s.echo.GET("/xrpc/app.bsky.feed.getFeed", s.handleProxyBskyFeedGetFeed, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware)
// admin routes
s.echo.POST("/xrpc/com.atproto.server.createInviteCode", s.handleCreateInviteCode, s.handleAdminMiddleware)
···
go s.backupRoutine()
+
go func() {
+
if err := s.requestCrawl(ctx); err != nil {
+
s.logger.Error("error requesting crawls", "err", err)
+
}
+
}()
+
+
<-ctx.Done()
+
+
fmt.Println("shut down")
+
+
return nil
+
}
+
+
func (s *Server) requestCrawl(ctx context.Context) error {
+
logger := s.logger.With("component", "request-crawl")
+
s.requestCrawlMu.Lock()
+
defer s.requestCrawlMu.Unlock()
+
+
logger.Info("requesting crawl with configured relays")
+
+
if time.Now().Sub(s.lastRequestCrawl) <= 1*time.Minute {
+
return fmt.Errorf("a crawl request has already been made within the last minute")
+
}
+
for _, relay := range s.config.Relays {
+
logger := logger.With("relay", relay)
+
logger.Info("requesting crawl from relay")
cli := xrpc.Client{Host: relay}
+
if err := atproto.SyncRequestCrawl(ctx, &cli, &atproto.SyncRequestCrawl_Input{
Hostname: s.config.Hostname,
+
}); err != nil {
+
logger.Error("error requesting crawl", "err", err)
+
} else {
+
logger.Info("crawl requested successfully")
+
}
}
+
s.lastRequestCrawl = time.Now()
return nil
}
+91
server/service_auth.go
···
···
+
package server
+
+
import (
+
"context"
+
"fmt"
+
"strings"
+
+
"github.com/bluesky-social/indigo/atproto/atcrypto"
+
"github.com/bluesky-social/indigo/atproto/identity"
+
atproto_identity "github.com/bluesky-social/indigo/atproto/identity"
+
"github.com/bluesky-social/indigo/atproto/syntax"
+
"github.com/golang-jwt/jwt/v4"
+
)
+
+
type ES256KSigningMethod struct {
+
alg string
+
}
+
+
func (m *ES256KSigningMethod) Alg() string {
+
return m.alg
+
}
+
+
func (m *ES256KSigningMethod) Verify(signingString string, signature string, key interface{}) error {
+
signatureBytes, err := jwt.DecodeSegment(signature)
+
if err != nil {
+
return err
+
}
+
return key.(atcrypto.PublicKey).HashAndVerifyLenient([]byte(signingString), signatureBytes)
+
}
+
+
func (m *ES256KSigningMethod) Sign(signingString string, key interface{}) (string, error) {
+
return "", fmt.Errorf("unimplemented")
+
}
+
+
func init() {
+
ES256K := ES256KSigningMethod{alg: "ES256K"}
+
jwt.RegisterSigningMethod(ES256K.Alg(), func() jwt.SigningMethod {
+
return &ES256K
+
})
+
}
+
+
func (s *Server) validateServiceAuth(ctx context.Context, rawToken string, nsid string) (string, error) {
+
token := strings.TrimSpace(rawToken)
+
+
parsedToken, err := jwt.ParseWithClaims(token, jwt.MapClaims{}, func(token *jwt.Token) (interface{}, error) {
+
did := syntax.DID(token.Claims.(jwt.MapClaims)["iss"].(string))
+
didDoc, err := s.passport.FetchDoc(ctx, did.String());
+
if err != nil {
+
return nil, fmt.Errorf("unable to resolve did %s: %s", did, err)
+
}
+
+
verificationMethods := make([]atproto_identity.DocVerificationMethod, len(didDoc.VerificationMethods))
+
for i, verificationMethod := range didDoc.VerificationMethods {
+
verificationMethods[i] = atproto_identity.DocVerificationMethod{
+
ID: verificationMethod.Id,
+
Type: verificationMethod.Type,
+
PublicKeyMultibase: verificationMethod.PublicKeyMultibase,
+
Controller: verificationMethod.Controller,
+
}
+
}
+
services := make([]atproto_identity.DocService, len(didDoc.Service))
+
for i, service := range didDoc.Service {
+
services[i] = atproto_identity.DocService{
+
ID: service.Id,
+
Type: service.Type,
+
ServiceEndpoint: service.ServiceEndpoint,
+
}
+
}
+
parsedIdentity := atproto_identity.ParseIdentity(&identity.DIDDocument{
+
DID: did,
+
AlsoKnownAs: didDoc.AlsoKnownAs,
+
VerificationMethod: verificationMethods,
+
Service: services,
+
})
+
+
key, err := parsedIdentity.PublicKey()
+
if err != nil {
+
return nil, fmt.Errorf("signing key not found for did %s: %s", did, err)
+
}
+
return key, nil
+
})
+
if err != nil {
+
return "", fmt.Errorf("invalid token: %s", err)
+
}
+
+
claims := parsedToken.Claims.(jwt.MapClaims)
+
if claims["lxm"] != nsid {
+
return "", fmt.Errorf("bad jwt lexicon method (\"lxm\"). must match: %s", nsid)
+
}
+
return claims["iss"].(string), nil
+
}
-18
sqlite_blockstore/sqlite_blockstore.go
···
func (bs *SqliteBlockstore) HashOnRead(enabled bool) {
panic("not implemented")
}
-
-
func (bs *SqliteBlockstore) Execute(ctx context.Context) error {
-
if !bs.readonly {
-
return fmt.Errorf("blockstore was not readonly")
-
}
-
-
bs.readonly = false
-
for _, b := range bs.inserts {
-
bs.Put(ctx, b)
-
}
-
bs.readonly = true
-
-
return nil
-
}
-
-
func (bs *SqliteBlockstore) GetLog() map[cid.Cid]blocks.Block {
-
return bs.inserts
-
}
···
func (bs *SqliteBlockstore) HashOnRead(enabled bool) {
panic("not implemented")
}