Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

domesday: identity directory service #940

Draft
wants to merge 5 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions HACKING.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ Run with, eg, `go run ./cmd/bigsky`):
- `cmd/sonar`: event stream monitoring tool
- `cmd/hepa`: auto-moderation rule engine service
- `cmd/rainbow`: firehose fanout service
- `cmd/domesbook`: identity directory service
- `gen`: dev tool to run CBOR type codegen

Packages:
Expand Down
4 changes: 4 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,10 @@ run-dev-relay: .env ## Runs 'bigsky' Relay for local dev
GOLOG_LOG_LEVEL=info go run ./cmd/bigsky --admin-key localdev
# --crawl-insecure-ws

.PHONY: run-dev-ident
run-dev-ident: .env ## Runs 'domesday' identity directory for local dev
GOLOG_LOG_LEVEL=info go run ./cmd/domesday serve

.PHONY: build-relay-image
build-relay-image: ## Builds 'bigsky' Relay docker image
docker build -t bigsky -f cmd/bigsky/Dockerfile .
Expand Down
17 changes: 17 additions & 0 deletions api/atproto/identitydefs.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

26 changes: 26 additions & 0 deletions api/atproto/identityrefreshIdentity.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

33 changes: 33 additions & 0 deletions api/atproto/identityresolveDid.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

27 changes: 27 additions & 0 deletions api/atproto/identityresolveIdentity.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

37 changes: 37 additions & 0 deletions cmd/domesday/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Run this dockerfile from the top level of the indigo git repository like:
#
# podman build -f ./cmd/domesday/Dockerfile -t domesday .

### Compile stage
FROM golang:1.23-alpine3.20 AS build-env
RUN apk add --no-cache build-base make git

ADD . /dockerbuild
WORKDIR /dockerbuild

# timezone data for alpine builds
ENV GOEXPERIMENT=loopvar
RUN GIT_VERSION=$(git describe --tags --long --always) && \
go build -tags timetzdata -o /domesday ./cmd/domesday

### Run stage
FROM alpine:3.20

RUN apk add --no-cache --update dumb-init ca-certificates
ENTRYPOINT ["dumb-init", "--"]

WORKDIR /
RUN mkdir -p data/domesday
COPY --from=build-env /domesday /

# small things to make golang binaries work well under alpine
ENV GODEBUG=netdns=go
ENV TZ=Etc/UTC

EXPOSE 2210

CMD ["/domesday", "run"]

LABEL org.opencontainers.image.source=https://github.com/bluesky-social/indigo
LABEL org.opencontainers.image.description="atproto identity directory (domesday)"
LABEL org.opencontainers.image.licenses=MIT
19 changes: 19 additions & 0 deletions cmd/domesday/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@

domesday: an atproto identity directory
========================================

This is a simple API server which caches atproto handle and DID resolution responses. It is useful when you have a bunch of services that do identity resolution, and you don't want duplicated caches.

The name is a reference to the [Domesday Book](https://en.wikipedia.org/wiki/Domesday_Book), an early manuscript recoding a English census in 1086. It is a big fancy book with a lot of names in it.

Available commands, flags, and config are documented in the usage (`--help`).

Current features and design decisions:

- all caches stored in Redis
- will consume from the firehose (but doesn't yet)
- Lexicon API endpoints:
- `GET com.atproto.identity.resolveHandle`
- `GET com.atproto.identity.resolveDid`
- `GET com.atproto.identity.resolveIdentity`
- `POST com.atproto.identity.refreshIdentity` (admin auth)
160 changes: 160 additions & 0 deletions cmd/domesday/handlers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
package main

import (
"fmt"
"log/slog"
"net/http"

comatproto "github.com/bluesky-social/indigo/api/atproto"
"github.com/bluesky-social/indigo/atproto/syntax"

"github.com/labstack/echo/v4"
)

type GenericError struct {
Error string `json:"error"`
Message string `json:"message"`
}

func (srv *Server) ResolveHandle(c echo.Context) error {
ctx := c.Request().Context()

hdl, err := syntax.ParseHandle(c.QueryParam("handle"))
if err != nil {
return c.JSON(400, GenericError{
Error: "InvalidHandleSyntax",
Message: fmt.Sprintf("%s", err), // TODO: something more idiomatic?
})
}

did, err := srv.dir.ResolveHandle(ctx, hdl)
if err != nil && false { // XXX: is ErrNotFound; other errors?
return c.JSON(404, GenericError{
Error: "HandleNotFound",
Message: fmt.Sprintf("%s", err),
})
} else if err != nil {
return c.JSON(500, GenericError{
Error: "InternalError",
Message: fmt.Sprintf("%s", err),
})
}
return c.JSON(200, comatproto.IdentityResolveHandle_Output{
Did: did.String(),
})
}

func (srv *Server) ResolveDid(c echo.Context) error {
ctx := c.Request().Context()

did, err := syntax.ParseDID(c.QueryParam("did"))
if err != nil {
return c.JSON(400, GenericError{
Error: "InvalidDidSyntax",
Message: fmt.Sprintf("%s", err), // TODO: something more idiomatic?
})
}

// XXX: ResolveDID() on identity?
doc, err := srv.dir.ResolveDID(ctx, did)
if err != nil && false { // XXX: is ErrNotFound; other errors?
return c.JSON(404, GenericError{
Error: "DidNotFound",
Message: fmt.Sprintf("%s", err),
})
} else if err != nil {
return c.JSON(500, GenericError{
Error: "InternalError",
Message: fmt.Sprintf("%s", err),
})
}
return c.JSON(200, comatproto.IdentityResolveDid_Output{
DidDoc: doc,
})
}

func (srv *Server) ResolveIdentity(c echo.Context) error {
ctx := c.Request().Context()

atid, err := syntax.ParseAtIdentifier(c.QueryParam("identifier"))
if err != nil {
return c.JSON(400, GenericError{
Error: "InvalidIdentifierSyntax",
Message: fmt.Sprintf("%s", err), // TODO: something more idiomatic?
})
}

// XXX: ResolveDID() on identity?
ident, err := srv.dir.Lookup(ctx, *atid)
if err != nil && false { // XXX: is ErrNotFound; other errors?
return c.JSON(404, GenericError{
Error: "DidNotFound",
Message: fmt.Sprintf("%s", err),
})
} else if err != nil {
return c.JSON(500, GenericError{
Error: "InternalError",
Message: fmt.Sprintf("%s", err),
})
}
handle := ident.Handle.String()
doc, err := srv.dir.ResolveDID(ctx, ident.DID)
if err != nil {
return err
}
return c.JSON(200, comatproto.IdentityDefs_AtprotoIdentity{
Did: ident.DID.String(),
Handle: &handle,
DidDoc: doc,
})
}

func (srv *Server) RefreshIdentity(c echo.Context) error {
ctx := c.Request().Context()

atid, err := syntax.ParseAtIdentifier(c.QueryParam("identifier"))
if err != nil {
return c.JSON(400, GenericError{
Error: "InvalidIdentifierSyntax",
Message: fmt.Sprintf("%s", err), // TODO: something more idiomatic?
})
}

err = srv.dir.Purge(ctx, *atid)
if err != nil {
return c.JSON(500, GenericError{
Error: "InternalError",
Message: fmt.Sprintf("%s", err),
})
}

return srv.ResolveIdentity(c)
}

type GenericStatus struct {
Daemon string `json:"daemon"`
Status string `json:"status"`
Message string `json:"msg,omitempty"`
}

func (srv *Server) errorHandler(err error, c echo.Context) {
code := http.StatusInternalServerError
var errorMessage string
if he, ok := err.(*echo.HTTPError); ok {
code = he.Code
errorMessage = fmt.Sprintf("%s", he.Message)
}
if code >= 500 {
slog.Warn("domesday-http-internal-error", "err", err)
}
// XXX: actual error struct
c.JSON(code, GenericStatus{Status: "error", Daemon: "domesday", Message: errorMessage})
}

func (s *Server) HandleHealthCheck(c echo.Context) error {
return c.JSON(200, GenericStatus{Status: "ok", Daemon: "domesday"})
}

func (srv *Server) WebHome(c echo.Context) error {
return c.JSON(200, GenericStatus{Status: "ok", Daemon: "domesday"})
}
Loading
Loading