porting all github actions from bluesky-social/indigo to tangled CI
at main 7.1 kB view raw
1package main 2 3import ( 4 "context" 5 "encoding/json" 6 "fmt" 7 "os" 8 "slices" 9 10 "github.com/bluesky-social/indigo/api/agnostic" 11 comatproto "github.com/bluesky-social/indigo/api/atproto" 12 "github.com/bluesky-social/indigo/atproto/crypto" 13 "github.com/bluesky-social/indigo/atproto/syntax" 14 15 "github.com/urfave/cli/v2" 16) 17 18var cmdAccountPlc = &cli.Command{ 19 Name: "plc", 20 Usage: "sub-commands for managing PLC DID via PDS host", 21 Flags: []cli.Flag{ 22 &cli.StringFlag{ 23 Name: "plc-host", 24 Usage: "method, hostname, and port of PLC registry", 25 Value: "https://plc.directory", 26 EnvVars: []string{"ATP_PLC_HOST"}, 27 }, 28 }, 29 Subcommands: []*cli.Command{ 30 &cli.Command{ 31 Name: "recommended", 32 Usage: "list recommended DID fields for current account", 33 Action: runAccountPlcRecommended, 34 }, 35 &cli.Command{ 36 Name: "request-token", 37 Usage: "request a 2FA token (by email) for signing op", 38 Action: runAccountPlcRequestToken, 39 }, 40 &cli.Command{ 41 Name: "sign", 42 Usage: "sign a PLC operation", 43 ArgsUsage: `<json-file>`, 44 Action: runAccountPlcSign, 45 Flags: []cli.Flag{ 46 &cli.StringFlag{ 47 Name: "token", 48 Usage: "2FA token for PLC operation signing request", 49 }, 50 }, 51 }, 52 &cli.Command{ 53 Name: "submit", 54 Usage: "submit a PLC operation (via PDS)", 55 ArgsUsage: `<json-file>`, 56 Action: runAccountPlcSubmit, 57 }, 58 &cli.Command{ 59 Name: "current", 60 Usage: "print current PLC data for account (fetched from directory)", 61 Action: runAccountPlcCurrent, 62 }, 63 &cli.Command{ 64 Name: "add-rotation-key", 65 Usage: "add a new rotation key to PLC identity (via PDS)", 66 ArgsUsage: `<pubkey>`, 67 Action: runAccountPlcAddRotationKey, 68 Flags: []cli.Flag{ 69 &cli.StringFlag{ 70 Name: "token", 71 Usage: "2FA token for PLC operation signing request", 72 }, 73 &cli.BoolFlag{ 74 Name: "first", 75 Usage: "inserts key at the top of key list (highest priority)", 76 }, 77 }, 78 }, 79 }, 80} 81 82func runAccountPlcRecommended(cctx *cli.Context) error { 83 ctx := context.Background() 84 85 xrpcc, err := loadAuthClient(ctx) 86 if err == ErrNoAuthSession { 87 return fmt.Errorf("auth required, but not logged in") 88 } else if err != nil { 89 return err 90 } 91 92 resp, err := agnostic.IdentityGetRecommendedDidCredentials(ctx, xrpcc) 93 if err != nil { 94 return err 95 } 96 97 b, err := json.MarshalIndent(resp, "", " ") 98 if err != nil { 99 return err 100 } 101 102 fmt.Println(string(b)) 103 return nil 104} 105 106func runAccountPlcRequestToken(cctx *cli.Context) error { 107 ctx := context.Background() 108 109 xrpcc, err := loadAuthClient(ctx) 110 if err == ErrNoAuthSession { 111 return fmt.Errorf("auth required, but not logged in") 112 } else if err != nil { 113 return err 114 } 115 116 err = comatproto.IdentityRequestPlcOperationSignature(ctx, xrpcc) 117 if err != nil { 118 return err 119 } 120 121 fmt.Println("Success; check email for token.") 122 return nil 123} 124 125func runAccountPlcSign(cctx *cli.Context) error { 126 ctx := context.Background() 127 128 opPath := cctx.Args().First() 129 if opPath == "" { 130 return fmt.Errorf("need to provide JSON file path as an argument") 131 } 132 133 xrpcc, err := loadAuthClient(ctx) 134 if err == ErrNoAuthSession { 135 return fmt.Errorf("auth required, but not logged in") 136 } else if err != nil { 137 return err 138 } 139 140 fileBytes, err := os.ReadFile(opPath) 141 if err != nil { 142 return err 143 } 144 145 var body agnostic.IdentitySignPlcOperation_Input 146 if err = json.Unmarshal(fileBytes, &body); err != nil { 147 return fmt.Errorf("failed decoding PLC op JSON: %w", err) 148 } 149 150 token := cctx.String("token") 151 if token != "" { 152 body.Token = &token 153 } 154 155 resp, err := agnostic.IdentitySignPlcOperation(ctx, xrpcc, &body) 156 if err != nil { 157 return err 158 } 159 160 b, err := json.MarshalIndent(resp.Operation, "", " ") 161 if err != nil { 162 return err 163 } 164 165 fmt.Println(string(b)) 166 return nil 167} 168 169func runAccountPlcSubmit(cctx *cli.Context) error { 170 ctx := context.Background() 171 172 opPath := cctx.Args().First() 173 if opPath == "" { 174 return fmt.Errorf("need to provide JSON file path as an argument") 175 } 176 177 xrpcc, err := loadAuthClient(ctx) 178 if err == ErrNoAuthSession { 179 return fmt.Errorf("auth required, but not logged in") 180 } else if err != nil { 181 return err 182 } 183 184 fileBytes, err := os.ReadFile(opPath) 185 if err != nil { 186 return err 187 } 188 189 var op json.RawMessage 190 if err = json.Unmarshal(fileBytes, &op); err != nil { 191 return fmt.Errorf("failed decoding PLC op JSON: %w", err) 192 } 193 194 err = agnostic.IdentitySubmitPlcOperation(ctx, xrpcc, &agnostic.IdentitySubmitPlcOperation_Input{ 195 Operation: &op, 196 }) 197 if err != nil { 198 return fmt.Errorf("failed submitting PLC op via PDS: %w", err) 199 } 200 201 return nil 202} 203 204func runAccountPlcCurrent(cctx *cli.Context) error { 205 ctx := context.Background() 206 207 xrpcc, err := loadAuthClient(ctx) 208 if err == ErrNoAuthSession || xrpcc.Auth == nil { 209 return fmt.Errorf("auth required, but not logged in") 210 } else if err != nil { 211 return err 212 } 213 214 did, err := syntax.ParseDID(xrpcc.Auth.Did) 215 if err != nil { 216 return err 217 } 218 219 plcData, err := fetchPLCData(ctx, cctx.String("plc-host"), did) 220 if err != nil { 221 return err 222 } 223 224 b, err := json.MarshalIndent(plcData, "", " ") 225 if err != nil { 226 return err 227 } 228 fmt.Println(string(b)) 229 return nil 230} 231 232func runAccountPlcAddRotationKey(cctx *cli.Context) error { 233 ctx := context.Background() 234 235 newKeyStr := cctx.Args().First() 236 if newKeyStr == "" { 237 return fmt.Errorf("need to provide public key argument (as did:key)") 238 } 239 240 // check that it is a valid pubkey 241 _, err := crypto.ParsePublicDIDKey(newKeyStr) 242 if err != nil { 243 return err 244 } 245 246 xrpcc, err := loadAuthClient(ctx) 247 if err == ErrNoAuthSession { 248 return fmt.Errorf("auth required, but not logged in") 249 } else if err != nil { 250 return err 251 } 252 253 did, err := syntax.ParseDID(xrpcc.Auth.Did) 254 if err != nil { 255 return err 256 } 257 258 // 1. fetch current PLC op: plc.directory/{did}/data 259 plcData, err := fetchPLCData(ctx, cctx.String("plc-host"), did) 260 if err != nil { 261 return err 262 } 263 264 if len(plcData.RotationKeys) >= 5 { 265 fmt.Println("WARNGING: already have 5 rotation keys, which is the maximum") 266 } 267 268 for _, k := range plcData.RotationKeys { 269 if k == newKeyStr { 270 return fmt.Errorf("key already registered as a rotation key") 271 } 272 } 273 274 // 2. update data 275 if cctx.Bool("first") { 276 plcData.RotationKeys = slices.Insert(plcData.RotationKeys, 0, newKeyStr) 277 } else { 278 plcData.RotationKeys = append(plcData.RotationKeys, newKeyStr) 279 } 280 281 // 3. get data signed (using token) 282 opBytes, err := json.Marshal(&plcData) 283 if err != nil { 284 return err 285 } 286 var body agnostic.IdentitySignPlcOperation_Input 287 if err = json.Unmarshal(opBytes, &body); err != nil { 288 return fmt.Errorf("failed decoding PLC op JSON: %w", err) 289 } 290 291 token := cctx.String("token") 292 if token != "" { 293 body.Token = &token 294 } 295 296 resp, err := agnostic.IdentitySignPlcOperation(ctx, xrpcc, &body) 297 if err != nil { 298 return err 299 } 300 301 // 4. submit signed op 302 err = agnostic.IdentitySubmitPlcOperation(ctx, xrpcc, &agnostic.IdentitySubmitPlcOperation_Input{ 303 Operation: resp.Operation, 304 }) 305 if err != nil { 306 return fmt.Errorf("failed submitting PLC op via PDS: %w", err) 307 } 308 309 fmt.Println("Success!") 310 return nil 311}