An atproto PDS written in Go

Complete migration onto cocoon (#37)

* Allow overwriting blocks during import repo

* Implement get recommended config

* Implement submit plc operation

authored by Ed Costello and committed by GitHub 35ee3465 99dd89ac

+31 -15
plc/client.go
··· 55 55 } 56 56 57 57 func (c *Client) CreateDID(sigkey *atcrypto.PrivateKeyK256, recovery string, handle string) (string, *Operation, error) { 58 - pubsigkey, err := sigkey.PublicKey() 58 + creds, err := c.CreateDidCredentials(sigkey, recovery, handle) 59 59 if err != nil { 60 60 return "", nil, err 61 61 } 62 62 63 - pubrotkey, err := c.rotationKey.PublicKey() 63 + op := Operation{ 64 + Type: "plc_operation", 65 + VerificationMethods: creds.VerificationMethods, 66 + RotationKeys: creds.RotationKeys, 67 + AlsoKnownAs: creds.AlsoKnownAs, 68 + Services: creds.Services, 69 + Prev: nil, 70 + } 71 + 72 + if err := c.SignOp(sigkey, &op); err != nil { 73 + return "", nil, err 74 + } 75 + 76 + did, err := DidFromOp(&op) 64 77 if err != nil { 65 78 return "", nil, err 66 79 } 67 80 81 + return did, &op, nil 82 + } 83 + 84 + func (c *Client) CreateDidCredentials(sigkey *atcrypto.PrivateKeyK256, recovery string, handle string) (*DidCredentials, error) { 85 + pubsigkey, err := sigkey.PublicKey() 86 + if err != nil { 87 + return nil, err 88 + } 89 + 90 + pubrotkey, err := c.rotationKey.PublicKey() 91 + if err != nil { 92 + return nil, err 93 + } 94 + 68 95 // todo 69 96 rotationKeys := []string{pubrotkey.DIDKey()} 70 97 if recovery != "" { ··· 77 104 }(recovery) 78 105 } 79 106 80 - op := Operation{ 81 - Type: "plc_operation", 107 + creds := DidCredentials{ 82 108 VerificationMethods: map[string]string{ 83 109 "atproto": pubsigkey.DIDKey(), 84 110 }, ··· 92 118 Endpoint: "https://" + c.pdsHostname, 93 119 }, 94 120 }, 95 - Prev: nil, 96 121 } 97 122 98 - if err := c.SignOp(sigkey, &op); err != nil { 99 - return "", nil, err 100 - } 101 - 102 - did, err := DidFromOp(&op) 103 - if err != nil { 104 - return "", nil, err 105 - } 106 - 107 - return did, &op, nil 123 + return &creds, nil 108 124 } 109 125 110 126 func (c *Client) SignOp(sigkey *atcrypto.PrivateKeyK256, op *Operation) error {
+8
plc/types.go
··· 8 8 cbg "github.com/whyrusleeping/cbor-gen" 9 9 ) 10 10 11 + 12 + type DidCredentials struct { 13 + VerificationMethods map[string]string `json:"verificationMethods"` 14 + RotationKeys []string `json:"rotationKeys"` 15 + AlsoKnownAs []string `json:"alsoKnownAs"` 16 + Services map[string]identity.OperationService `json:"services"` 17 + } 18 + 11 19 type Operation struct { 12 20 Type string `json:"type"` 13 21 VerificationMethods map[string]string `json:"verificationMethods"`
+87
server/handle_identity_submit_plc_operation.go
··· 1 + package server 2 + 3 + import ( 4 + "context" 5 + "slices" 6 + "strings" 7 + "time" 8 + 9 + "github.com/bluesky-social/indigo/api/atproto" 10 + "github.com/bluesky-social/indigo/atproto/atcrypto" 11 + "github.com/bluesky-social/indigo/events" 12 + "github.com/bluesky-social/indigo/util" 13 + "github.com/haileyok/cocoon/internal/helpers" 14 + "github.com/haileyok/cocoon/models" 15 + "github.com/haileyok/cocoon/plc" 16 + "github.com/labstack/echo/v4" 17 + ) 18 + 19 + type ComAtprotoSubmitPlcOperationRequest struct { 20 + Operation plc.Operation `json:"operation"` 21 + } 22 + 23 + func (s *Server) handleSubmitPlcOperation(e echo.Context) error { 24 + repo := e.Get("repo").(*models.RepoActor) 25 + 26 + var req ComAtprotoSubmitPlcOperationRequest 27 + if err := e.Bind(&req); err != nil { 28 + s.logger.Error("error binding", "error", err) 29 + return helpers.ServerError(e, nil) 30 + } 31 + 32 + if err := e.Validate(req); err != nil { 33 + return helpers.InputError(e, nil) 34 + } 35 + if !strings.HasPrefix(repo.Repo.Did, "did:plc:") { 36 + return helpers.InputError(e, nil) 37 + } 38 + 39 + op := req.Operation; 40 + 41 + k, err := atcrypto.ParsePrivateBytesK256(repo.SigningKey) 42 + if err != nil { 43 + s.logger.Error("error parsing key", "error", err) 44 + return helpers.ServerError(e, nil) 45 + } 46 + required, err := s.plcClient.CreateDidCredentials(k, "", repo.Actor.Handle) 47 + if err != nil { 48 + s.logger.Error("error crating did credentials", "error", err) 49 + return helpers.ServerError(e, nil) 50 + } 51 + 52 + for _, expectedKey := range required.RotationKeys { 53 + if !slices.Contains(op.RotationKeys, expectedKey) { 54 + return helpers.InputError(e, nil) 55 + } 56 + } 57 + if op.Services["atproto_pds"].Type != "AtprotoPersonalDataServer" { 58 + return helpers.InputError(e, nil) 59 + } 60 + if op.Services["atproto_pds"].Endpoint != required.Services["atproto_pds"].Endpoint { 61 + return helpers.InputError(e, nil) 62 + } 63 + if op.VerificationMethods["atproto"] != required.VerificationMethods["atproto"] { 64 + return helpers.InputError(e, nil) 65 + } 66 + if op.AlsoKnownAs[0] != required.AlsoKnownAs[0] { 67 + return helpers.InputError(e, nil) 68 + } 69 + 70 + if err := s.plcClient.SendOperation(e.Request().Context(), repo.Repo.Did, &op); err != nil { 71 + return err 72 + } 73 + 74 + if err := s.passport.BustDoc(context.TODO(), repo.Repo.Did); err != nil { 75 + s.logger.Warn("error busting did doc", "error", err) 76 + } 77 + 78 + s.evtman.AddEvent(context.TODO(), &events.XRPCStreamEvent{ 79 + RepoIdentity: &atproto.SyncSubscribeRepos_Identity{ 80 + Did: repo.Repo.Did, 81 + Seq: time.Now().UnixMicro(), // TODO: no 82 + Time: time.Now().Format(util.ISO8601), 83 + }, 84 + }) 85 + 86 + return nil 87 + }
+1 -1
server/handle_import_repo.go
··· 87 87 Value: b.RawData(), 88 88 } 89 89 90 - if err := tx.Create(rec).Error; err != nil { 90 + if err := tx.Save(rec).Error; err != nil { 91 91 return err 92 92 } 93 93
+2
server/server.go
··· 459 459 s.echo.GET("/xrpc/com.atproto.server.getSession", s.handleGetSession, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware) 460 460 s.echo.POST("/xrpc/com.atproto.server.refreshSession", s.handleRefreshSession, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware) 461 461 s.echo.POST("/xrpc/com.atproto.server.deleteSession", s.handleDeleteSession, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware) 462 + s.echo.GET("/xrpc/com.atproto.identity.getRecommendedDidCredentials", s.handleGetRecommendedDidCredentials, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware) 462 463 s.echo.POST("/xrpc/com.atproto.identity.updateHandle", s.handleIdentityUpdateHandle, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware) 464 + s.echo.POST("/xrpc/com.atproto.identity.submitPlcOperation", s.handleSubmitPlcOperation, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware) 463 465 s.echo.POST("/xrpc/com.atproto.server.confirmEmail", s.handleServerConfirmEmail, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware) 464 466 s.echo.POST("/xrpc/com.atproto.server.requestEmailConfirmation", s.handleServerRequestEmailConfirmation, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware) 465 467 s.echo.POST("/xrpc/com.atproto.server.requestPasswordReset", s.handleServerRequestPasswordReset) // AUTH NOT REQUIRED FOR THIS ONE