From 35ee3465c3e68e9bb09b40c0ee311ff7bbf83a35 Mon Sep 17 00:00:00 2001 From: Ed Costello Date: Wed, 29 Oct 2025 04:20:45 +1300 Subject: [PATCH] Complete migration onto cocoon (#37) * Allow overwriting blocks during import repo * Implement get recommended config * Implement submit plc operation --- plc/client.go | 46 ++++++---- plc/types.go | 8 ++ ...dentity_get_recommended_did_credentials.go | 24 +++++ .../handle_identity_submit_plc_operation.go | 87 +++++++++++++++++++ server/handle_import_repo.go | 2 +- server/server.go | 2 + 6 files changed, 153 insertions(+), 16 deletions(-) create mode 100644 server/handle_identity_get_recommended_did_credentials.go create mode 100644 server/handle_identity_submit_plc_operation.go diff --git a/plc/client.go b/plc/client.go index 07677c1..ceb00a1 100644 --- a/plc/client.go +++ b/plc/client.go @@ -55,16 +55,43 @@ func NewClient(args *ClientArgs) (*Client, error) { } func (c *Client) CreateDID(sigkey *atcrypto.PrivateKeyK256, recovery string, handle string) (string, *Operation, error) { - pubsigkey, err := sigkey.PublicKey() + creds, err := c.CreateDidCredentials(sigkey, recovery, handle) if err != nil { return "", nil, err } - pubrotkey, err := c.rotationKey.PublicKey() + op := Operation{ + Type: "plc_operation", + VerificationMethods: creds.VerificationMethods, + RotationKeys: creds.RotationKeys, + AlsoKnownAs: creds.AlsoKnownAs, + Services: creds.Services, + Prev: nil, + } + + if err := c.SignOp(sigkey, &op); err != nil { + return "", nil, err + } + + did, err := DidFromOp(&op) if err != nil { return "", nil, err } + return did, &op, nil +} + +func (c *Client) CreateDidCredentials(sigkey *atcrypto.PrivateKeyK256, recovery string, handle string) (*DidCredentials, error) { + pubsigkey, err := sigkey.PublicKey() + if err != nil { + return nil, err + } + + pubrotkey, err := c.rotationKey.PublicKey() + if err != nil { + return nil, err + } + // todo rotationKeys := []string{pubrotkey.DIDKey()} if recovery != "" { @@ -77,8 +104,7 @@ func (c *Client) CreateDID(sigkey *atcrypto.PrivateKeyK256, recovery string, han }(recovery) } - op := Operation{ - Type: "plc_operation", + creds := DidCredentials{ VerificationMethods: map[string]string{ "atproto": pubsigkey.DIDKey(), }, @@ -92,19 +118,9 @@ func (c *Client) CreateDID(sigkey *atcrypto.PrivateKeyK256, recovery string, han Endpoint: "https://" + c.pdsHostname, }, }, - Prev: nil, } - if err := c.SignOp(sigkey, &op); err != nil { - return "", nil, err - } - - did, err := DidFromOp(&op) - if err != nil { - return "", nil, err - } - - return did, &op, nil + return &creds, nil } func (c *Client) SignOp(sigkey *atcrypto.PrivateKeyK256, op *Operation) error { diff --git a/plc/types.go b/plc/types.go index 0137e03..7d2eeb5 100644 --- a/plc/types.go +++ b/plc/types.go @@ -8,6 +8,14 @@ import ( cbg "github.com/whyrusleeping/cbor-gen" ) + +type DidCredentials struct { + VerificationMethods map[string]string `json:"verificationMethods"` + RotationKeys []string `json:"rotationKeys"` + AlsoKnownAs []string `json:"alsoKnownAs"` + Services map[string]identity.OperationService `json:"services"` +} + type Operation struct { Type string `json:"type"` VerificationMethods map[string]string `json:"verificationMethods"` diff --git a/server/handle_identity_get_recommended_did_credentials.go b/server/handle_identity_get_recommended_did_credentials.go new file mode 100644 index 0000000..f6a6c47 --- /dev/null +++ b/server/handle_identity_get_recommended_did_credentials.go @@ -0,0 +1,24 @@ +package server + +import ( + "github.com/bluesky-social/indigo/atproto/atcrypto" + "github.com/haileyok/cocoon/internal/helpers" + "github.com/haileyok/cocoon/models" + "github.com/labstack/echo/v4" +) + +func (s *Server) handleGetRecommendedDidCredentials(e echo.Context) error { + repo := e.Get("repo").(*models.RepoActor) + k, err := atcrypto.ParsePrivateBytesK256(repo.SigningKey) + if err != nil { + s.logger.Error("error parsing key", "error", err) + return helpers.ServerError(e, nil) + } + creds, 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) + } + + return e.JSON(200, creds) +} diff --git a/server/handle_identity_submit_plc_operation.go b/server/handle_identity_submit_plc_operation.go new file mode 100644 index 0000000..33d1e6f --- /dev/null +++ b/server/handle_identity_submit_plc_operation.go @@ -0,0 +1,87 @@ +package server + +import ( + "context" + "slices" + "strings" + "time" + + "github.com/bluesky-social/indigo/api/atproto" + "github.com/bluesky-social/indigo/atproto/atcrypto" + "github.com/bluesky-social/indigo/events" + "github.com/bluesky-social/indigo/util" + "github.com/haileyok/cocoon/internal/helpers" + "github.com/haileyok/cocoon/models" + "github.com/haileyok/cocoon/plc" + "github.com/labstack/echo/v4" +) + +type ComAtprotoSubmitPlcOperationRequest struct { + Operation plc.Operation `json:"operation"` +} + +func (s *Server) handleSubmitPlcOperation(e echo.Context) error { + repo := e.Get("repo").(*models.RepoActor) + + var req ComAtprotoSubmitPlcOperationRequest + if err := e.Bind(&req); err != nil { + s.logger.Error("error binding", "error", err) + return helpers.ServerError(e, nil) + } + + if err := e.Validate(req); err != nil { + return helpers.InputError(e, nil) + } + if !strings.HasPrefix(repo.Repo.Did, "did:plc:") { + return helpers.InputError(e, nil) + } + + op := req.Operation; + + k, err := atcrypto.ParsePrivateBytesK256(repo.SigningKey) + if err != nil { + s.logger.Error("error parsing key", "error", err) + return helpers.ServerError(e, nil) + } + required, err := s.plcClient.CreateDidCredentials(k, "", repo.Actor.Handle) + if err != nil { + s.logger.Error("error crating did credentials", "error", err) + return helpers.ServerError(e, nil) + } + + for _, expectedKey := range required.RotationKeys { + if !slices.Contains(op.RotationKeys, expectedKey) { + return helpers.InputError(e, nil) + } + } + if op.Services["atproto_pds"].Type != "AtprotoPersonalDataServer" { + return helpers.InputError(e, nil) + } + if op.Services["atproto_pds"].Endpoint != required.Services["atproto_pds"].Endpoint { + return helpers.InputError(e, nil) + } + if op.VerificationMethods["atproto"] != required.VerificationMethods["atproto"] { + return helpers.InputError(e, nil) + } + if op.AlsoKnownAs[0] != required.AlsoKnownAs[0] { + return helpers.InputError(e, nil) + } + + if err := s.plcClient.SendOperation(e.Request().Context(), repo.Repo.Did, &op); err != nil { + return err + } + + if err := s.passport.BustDoc(context.TODO(), repo.Repo.Did); err != nil { + s.logger.Warn("error busting did doc", "error", err) + } + + s.evtman.AddEvent(context.TODO(), &events.XRPCStreamEvent{ + RepoIdentity: &atproto.SyncSubscribeRepos_Identity{ + Did: repo.Repo.Did, + Seq: time.Now().UnixMicro(), // TODO: no + Time: time.Now().Format(util.ISO8601), + }, + }) + + return nil +} diff --git a/server/handle_import_repo.go b/server/handle_import_repo.go index b54aa68..e762846 100644 --- a/server/handle_import_repo.go +++ b/server/handle_import_repo.go @@ -87,7 +87,7 @@ func (s *Server) handleRepoImportRepo(e echo.Context) error { Value: b.RawData(), } - if err := tx.Create(rec).Error; err != nil { + if err := tx.Save(rec).Error; err != nil { return err } diff --git a/server/server.go b/server/server.go index 749b31b..362072a 100644 --- a/server/server.go +++ b/server/server.go @@ -459,7 +459,9 @@ func (s *Server) addRoutes() { s.echo.GET("/xrpc/com.atproto.server.getSession", s.handleGetSession, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware) s.echo.POST("/xrpc/com.atproto.server.refreshSession", s.handleRefreshSession, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware) s.echo.POST("/xrpc/com.atproto.server.deleteSession", s.handleDeleteSession, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware) + s.echo.GET("/xrpc/com.atproto.identity.getRecommendedDidCredentials", s.handleGetRecommendedDidCredentials, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware) s.echo.POST("/xrpc/com.atproto.identity.updateHandle", s.handleIdentityUpdateHandle, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware) + s.echo.POST("/xrpc/com.atproto.identity.submitPlcOperation", s.handleSubmitPlcOperation, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware) s.echo.POST("/xrpc/com.atproto.server.confirmEmail", s.handleServerConfirmEmail, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware) s.echo.POST("/xrpc/com.atproto.server.requestEmailConfirmation", s.handleServerRequestEmailConfirmation, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware) s.echo.POST("/xrpc/com.atproto.server.requestPasswordReset", s.handleServerRequestPasswordReset) // AUTH NOT REQUIRED FOR THIS ONE -- 2.51.2