+164
cmd/goat/plc.go
+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
+
}