An atproto PDS written in Go

Compare changes

Choose any two refs to compare.

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