porting all github actions from bluesky-social/indigo to tangled CI

goat plc update

Changed files
+164
cmd
goat
+164
cmd/goat/plc.go
··· 136 136 }, 137 137 Action: runPLCSubmit, 138 138 }, 139 + &cli.Command{ 140 + Name: "update", 141 + Usage: "apply updates to a previous operation produce a new one (but don't sign or submit it, yet)", 142 + ArgsUsage: `<DID>`, 143 + Flags: []cli.Flag{ 144 + &cli.StringFlag{ 145 + Name: "prev", 146 + Usage: "the CID of the operation to use as a base (uses most recent op if not specified)", 147 + }, 148 + &cli.StringFlag{ 149 + Name: "handle", 150 + Usage: "atproto handle", 151 + }, 152 + &cli.StringSliceFlag{ 153 + Name: "add-rotation-key", 154 + Usage: "rotation public key, in did:key format (added to front of rotationKey list)", 155 + }, 156 + &cli.StringSliceFlag{ 157 + Name: "remove-rotation-key", 158 + Usage: "rotation public key, in did:key format", 159 + }, 160 + &cli.StringFlag{ 161 + Name: "atproto-key", 162 + Usage: "atproto repo signing public key, in did:key format", 163 + }, 164 + &cli.StringFlag{ 165 + Name: "pds", 166 + Usage: "atproto PDS service URL", 167 + }, 168 + }, 169 + Action: runPLCUpdate, 170 + }, 139 171 }, 140 172 } 141 173 ··· 594 626 595 627 return nil 596 628 } 629 + 630 + // fetch logs from /log/audit, select according to base_cid ("" means use latest), and 631 + // prepare it for updates: 632 + // - convert from legacy op format if needed (and reject tombstone ops) 633 + // - strip signature 634 + // - set `prev` to appropriate value 635 + func fetchOpForUpdate(ctx context.Context, c didplc.Client, did string, base_cid string) (*didplc.RegularOp, error) { 636 + auditlog, err := c.OpLog(ctx, did, true) // NB: this API changes in a pending go-didplc PR 637 + if err != nil { 638 + return nil, err 639 + } 640 + 641 + if err = didplc.VerifyOpLog(auditlog); err != nil { 642 + return nil, err 643 + } 644 + 645 + var baseLogEntry *didplc.LogEntry 646 + if base_cid == "" { 647 + // use most recent entry 648 + baseLogEntry = &auditlog[len(auditlog)-1] 649 + } else { 650 + // scan for the specified entry 651 + for _, entry := range auditlog { 652 + if entry.CID == base_cid { 653 + baseLogEntry = &entry 654 + break 655 + } 656 + } 657 + if baseLogEntry == nil { 658 + return nil, fmt.Errorf("no operation found matching CID %s", base_cid) 659 + } 660 + } 661 + var op didplc.RegularOp 662 + switch baseOp := baseLogEntry.Operation.AsOperation().(type) { 663 + case *didplc.RegularOp: 664 + op = *baseOp 665 + op.Sig = nil 666 + case *didplc.LegacyOp: 667 + op = baseOp.RegularOp() 668 + case *didplc.TombstoneOp: 669 + return nil, fmt.Errorf("cannot update from a tombstone op") 670 + } 671 + op.Prev = &baseLogEntry.CID 672 + return &op, nil 673 + } 674 + 675 + func runPLCUpdate(cctx *cli.Context) error { 676 + ctx := context.Background() 677 + prevCID := cctx.String("prev") 678 + 679 + didString := cctx.Args().First() 680 + if didString == "" { 681 + return fmt.Errorf("please specify a DID to update") 682 + } 683 + 684 + c := didplc.Client{ 685 + DirectoryURL: cctx.String("plc-host"), 686 + UserAgent: GOAT_PLC_USER_AGENT, 687 + } 688 + op, err := fetchOpForUpdate(ctx, c, didString, prevCID) 689 + if err != nil { 690 + return err 691 + } 692 + 693 + for _, rotationKey := range cctx.StringSlice("remove-rotation-key") { 694 + if _, err := crypto.ParsePublicDIDKey(rotationKey); err != nil { 695 + return err 696 + } 697 + removeSuccess := false 698 + for idx, existingRotationKey := range op.RotationKeys { 699 + if existingRotationKey == rotationKey { 700 + op.RotationKeys = append(op.RotationKeys[:idx], op.RotationKeys[idx+1:]...) 701 + removeSuccess = true 702 + } 703 + } 704 + if !removeSuccess { 705 + return fmt.Errorf("failed remove rotation key %s, not found in array", rotationKey) 706 + } 707 + } 708 + 709 + for _, rotationKey := range cctx.StringSlice("add-rotation-key") { 710 + if _, err := crypto.ParsePublicDIDKey(rotationKey); err != nil { 711 + return err 712 + } 713 + // prepend (Note: if adding multiple rotation keys at once, they'll end up in reverse order) 714 + op.RotationKeys = append([]string{rotationKey}, op.RotationKeys...) 715 + } 716 + 717 + handle := cctx.String("handle") 718 + if handle != "" { 719 + parsedHandle, err := syntax.ParseHandle(strings.TrimPrefix(handle, "at://")) 720 + if err != nil { 721 + return err 722 + } 723 + 724 + // strip any existing at:// akas 725 + // (someone might have some non-atproto akas, we will leave them untouched, 726 + // they can manually manage those or use some other tool if needed) 727 + var akas []string 728 + for _, aka := range op.AlsoKnownAs { 729 + if !strings.HasPrefix(aka, "at://") { 730 + akas = append(akas, aka) 731 + } 732 + } 733 + op.AlsoKnownAs = append(akas, "at://"+string(parsedHandle)) 734 + } 735 + 736 + atprotoKey := cctx.String("atproto-key") 737 + if atprotoKey != "" { 738 + if _, err := crypto.ParsePublicDIDKey(atprotoKey); err != nil { 739 + return err 740 + } 741 + op.VerificationMethods["atproto"] = atprotoKey 742 + } 743 + 744 + pds := cctx.String("pds") 745 + if pds != "" { 746 + // TODO: check pds is valid URI? 747 + op.Services["atproto_pds"] = didplc.OpService{ 748 + Type: "AtprotoPersonalDataServer", 749 + Endpoint: pds, 750 + } 751 + } 752 + 753 + res, err := json.MarshalIndent(op, "", " ") 754 + if err != nil { 755 + return err 756 + } 757 + fmt.Println(string(res)) 758 + 759 + return nil 760 + }