A fork of the Cocoon PDS but being made more distributed.
11

Configure Feed

Select the types of activity you want to include in your feed.

Sign plc operations to allow GOAT migration with cocoon as source (#39)

authored by

Ed Costello and committed by
GitHub
(Oct 29, 2025, 7:03 AM -0700) 64fa62eb 9fe89b4b

+160 -5
+5 -5
README.md
··· 143 143 144 144 ### Identity 145 145 146 - - [ ] `com.atproto.identity.getRecommendedDidCredentials` 147 - - [ ] `com.atproto.identity.requestPlcOperationSignature` 146 + - [x] `com.atproto.identity.getRecommendedDidCredentials` 147 + - [x] `com.atproto.identity.requestPlcOperationSignature` 148 148 - [x] `com.atproto.identity.resolveHandle` 149 - - [ ] `com.atproto.identity.signPlcOperation` 150 - - [ ] `com.atproto.identity.submitPlcOperation` 149 + - [x] `com.atproto.identity.signPlcOperation` 150 + - [x] `com.atproto.identity.submitPlcOperation` 151 151 - [x] `com.atproto.identity.updateHandle` 152 152 153 153 ### Repo ··· 158 158 - [x] `com.atproto.repo.deleteRecord` 159 159 - [x] `com.atproto.repo.describeRepo` 160 160 - [x] `com.atproto.repo.getRecord` 161 - - [x] `com.atproto.repo.importRepo` (Works "okay". You still have to handle PLC operations on your own when migrating. Use with extreme caution.) 161 + - [x] `com.atproto.repo.importRepo` (Works "okay". Use with extreme caution.) 162 162 - [x] `com.atproto.repo.listRecords` 163 163 - [ ] `com.atproto.repo.listMissingBlobs` 164 164
+2
models/models.go
··· 19 19 EmailUpdateCodeExpiresAt *time.Time 20 20 PasswordResetCode *string 21 21 PasswordResetCodeExpiresAt *time.Time 22 + PlcOperationCode *string 23 + PlcOperationCodeExpiresAt *time.Time 22 24 Password string 23 25 SigningKey []byte 24 26 Rev string
+29
server/handle_identity_request_plc_operation.go
··· 1 + package server 2 + 3 + import ( 4 + "fmt" 5 + "time" 6 + 7 + "github.com/haileyok/cocoon/internal/helpers" 8 + "github.com/haileyok/cocoon/models" 9 + "github.com/labstack/echo/v4" 10 + ) 11 + 12 + func (s *Server) handleIdentityRequestPlcOperationSignature(e echo.Context) error { 13 + urepo := e.Get("repo").(*models.RepoActor) 14 + 15 + code := fmt.Sprintf("%s-%s", helpers.RandomVarchar(5), helpers.RandomVarchar(5)) 16 + eat := time.Now().Add(10 * time.Minute).UTC() 17 + 18 + 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 { 19 + s.logger.Error("error updating user", "error", err) 20 + return helpers.ServerError(e, nil) 21 + } 22 + 23 + if err := s.sendPlcTokenReset(urepo.Email, urepo.Handle, code); err != nil { 24 + s.logger.Error("error sending mail", "error", err) 25 + return helpers.ServerError(e, nil) 26 + } 27 + 28 + return e.NoContent(200) 29 + }
+103
server/handle_identity_sign_plc_operation.go
··· 1 + package server 2 + 3 + import ( 4 + "context" 5 + "strings" 6 + "time" 7 + 8 + "github.com/Azure/go-autorest/autorest/to" 9 + "github.com/bluesky-social/indigo/atproto/atcrypto" 10 + "github.com/haileyok/cocoon/identity" 11 + "github.com/haileyok/cocoon/internal/helpers" 12 + "github.com/haileyok/cocoon/models" 13 + "github.com/haileyok/cocoon/plc" 14 + "github.com/labstack/echo/v4" 15 + ) 16 + 17 + type ComAtprotoSignPlcOperationRequest struct { 18 + Token string `json:"token"` 19 + VerificationMethods *map[string]string `json:"verificationMethods"` 20 + RotationKeys *[]string `json:"rotationKeys"` 21 + AlsoKnownAs *[]string `json:"alsoKnownAs"` 22 + Services *map[string]identity.OperationService `json:"services"` 23 + } 24 + 25 + type ComAtprotoSignPlcOperationResponse struct { 26 + Operation plc.Operation `json:"operation"` 27 + } 28 + 29 + func (s *Server) handleSignPlcOperation(e echo.Context) error { 30 + repo := e.Get("repo").(*models.RepoActor) 31 + 32 + var req ComAtprotoSignPlcOperationRequest 33 + if err := e.Bind(&req); err != nil { 34 + s.logger.Error("error binding", "error", err) 35 + return helpers.ServerError(e, nil) 36 + } 37 + 38 + if !strings.HasPrefix(repo.Repo.Did, "did:plc:") { 39 + return helpers.InputError(e, nil) 40 + } 41 + 42 + if repo.PlcOperationCode == nil || repo.PlcOperationCodeExpiresAt == nil { 43 + return helpers.InputError(e, to.StringPtr("InvalidToken")) 44 + } 45 + 46 + if *repo.PlcOperationCode != req.Token { 47 + return helpers.InvalidTokenError(e) 48 + } 49 + 50 + if time.Now().UTC().After(*repo.PlcOperationCodeExpiresAt) { 51 + return helpers.ExpiredTokenError(e) 52 + } 53 + 54 + ctx := context.WithValue(e.Request().Context(), "skip-cache", true) 55 + log, err := identity.FetchDidAuditLog(ctx, nil, repo.Repo.Did) 56 + if err != nil { 57 + s.logger.Error("error fetching doc", "error", err) 58 + return helpers.ServerError(e, nil) 59 + } 60 + 61 + latest := log[len(log)-1] 62 + 63 + op := plc.Operation{ 64 + Type: "plc_operation", 65 + VerificationMethods: latest.Operation.VerificationMethods, 66 + RotationKeys: latest.Operation.RotationKeys, 67 + AlsoKnownAs: latest.Operation.AlsoKnownAs, 68 + Services: latest.Operation.Services, 69 + Prev: &latest.Cid, 70 + } 71 + if req.VerificationMethods != nil { 72 + op.VerificationMethods = *req.VerificationMethods 73 + } 74 + if req.RotationKeys != nil { 75 + op.RotationKeys = *req.RotationKeys 76 + } 77 + if req.AlsoKnownAs != nil { 78 + op.AlsoKnownAs = *req.AlsoKnownAs 79 + } 80 + if req.Services != nil { 81 + op.Services = *req.Services 82 + } 83 + 84 + k, err := atcrypto.ParsePrivateBytesK256(repo.SigningKey) 85 + if err != nil { 86 + s.logger.Error("error parsing signing key", "error", err) 87 + return helpers.ServerError(e, nil) 88 + } 89 + 90 + if err := s.plcClient.SignOp(k, &op); err != nil { 91 + s.logger.Error("error signing plc operation", "error", err) 92 + return helpers.ServerError(e, nil) 93 + } 94 + 95 + 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 { 96 + s.logger.Error("error updating repo", "error", err) 97 + return helpers.ServerError(e, nil) 98 + } 99 + 100 + return e.JSON(200, ComAtprotoSignPlcOperationResponse{ 101 + Operation: op, 102 + }) 103 + }
+19
server/mail.go
··· 40 40 return nil 41 41 } 42 42 43 + func (s *Server) sendPlcTokenReset(email, handle, code string) error { 44 + if s.mail == nil { 45 + return nil 46 + } 47 + 48 + s.mailLk.Lock() 49 + defer s.mailLk.Unlock() 50 + 51 + s.mail.To(email) 52 + s.mail.Subject("PLC token for " + s.config.Hostname) 53 + s.mail.Plain().Set(fmt.Sprintf("Hello %s. Your PLC operation code is %s. This code will expire in ten minutes.", handle, code)) 54 + 55 + if err := s.mail.Send(); err != nil { 56 + return err 57 + } 58 + 59 + return nil 60 + } 61 + 43 62 func (s *Server) sendEmailUpdate(email, handle, code string) error { 44 63 if s.mail == nil { 45 64 return nil
+2
server/server.go
··· 461 461 s.echo.POST("/xrpc/com.atproto.server.deleteSession", s.handleDeleteSession, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware) 462 462 s.echo.GET("/xrpc/com.atproto.identity.getRecommendedDidCredentials", s.handleGetRecommendedDidCredentials, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware) 463 463 s.echo.POST("/xrpc/com.atproto.identity.updateHandle", s.handleIdentityUpdateHandle, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware) 464 + s.echo.POST("/xrpc/com.atproto.identity.requestPlcOperationSignature", s.handleIdentityRequestPlcOperationSignature, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware) 465 + s.echo.POST("/xrpc/com.atproto.identity.signPlcOperation", s.handleSignPlcOperation, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware) 464 466 s.echo.POST("/xrpc/com.atproto.identity.submitPlcOperation", s.handleSubmitPlcOperation, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware) 465 467 s.echo.POST("/xrpc/com.atproto.server.confirmEmail", s.handleServerConfirmEmail, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware) 466 468 s.echo.POST("/xrpc/com.atproto.server.requestEmailConfirmation", s.handleServerRequestEmailConfirmation, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware)