Monorepo for Tangled tangled.org
2

Configure Feed

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

appview: handle non-unique publicKey records

- upsert public key to handle record update event
- don't delete by pair of name and key. delete by name or rkey instead.

Signed-off-by: Seongmin Lee <git@boltless.me>

author
Seongmin Lee
date (Jun 25, 2026, 2:51 AM +0900) commit 67dfcb8a parent 67b55bae change-id zytsuzsp
+106 -57
+17 -7
appview/db/pubkeys.go
··· 6 6 "tangled.org/core/appview/models" 7 7 ) 8 8 9 - func AddPublicKey(e Execer, did, name, key, rkey string) error { 9 + func UpsertPublicKey(e Execer, pubKey models.PublicKey) error { 10 10 _, err := e.Exec( 11 - `insert or ignore into public_keys (did, name, key, rkey) 12 - values (?, ?, ?, ?)`, 13 - did, name, key, rkey) 11 + `insert into public_keys (did, rkey, name, key, created) 12 + values (?, ?, ?, ?, ?) 13 + on conflict(did, rkey) do update set 14 + name = excluded.name, 15 + key = excluded.key, 16 + created = excluded.created`, 17 + pubKey.Did, 18 + pubKey.Rkey, 19 + pubKey.Name, 20 + pubKey.Key, 21 + pubKey.Created.Format(time.RFC3339), 22 + ) 14 23 return err 15 24 } 16 25 ··· 21 30 return err 22 31 } 23 32 24 - func DeletePublicKey(e Execer, did, name, key string) error { 33 + // for public_keys with empty rkey 34 + func DeletePublicKeyLegacy(e Execer, did, name string) error { 25 35 _, err := e.Exec(` 26 36 delete from public_keys 27 - where did = ? and name = ? and key = ?`, 28 - did, name, key) 37 + where did = ? and name = ? and rkey = ''`, 38 + did, name) 29 39 return err 30 40 } 31 41
+10 -13
appview/ingester.go
··· 244 244 if err != nil { 245 245 return fmt.Errorf("failed to %s star record: %w", e.Commit.Operation, err) 246 246 } 247 + l.Info("processed star", "operation", e.Commit.Operation, "rkey", e.Commit.RKey) 247 248 248 249 l.Info("ingested record") 249 250 return nil ··· 277 278 if err != nil { 278 279 return fmt.Errorf("failed to %s follow record: %w", e.Commit.Operation, err) 279 280 } 281 + l.Info("processed follow", "operation", e.Commit.Operation, "rkey", e.Commit.RKey) 280 282 281 283 l.Info("ingested record") 282 284 return nil ··· 382 384 l = l.With("handler", "ingestPublicKey") 383 385 384 386 switch e.Commit.Operation { 385 - case jmodels.CommitOperationCreate: 387 + case jmodels.CommitOperationCreate, jmodels.CommitOperationUpdate: 386 388 l.Debug("processing add of pubkey") 387 389 raw := json.RawMessage(e.Commit.Record) 388 390 record := tangled.PublicKey{} ··· 391 393 l.Error("invalid record", "err", err) 392 394 return err 393 395 } 394 - 395 - name := record.Name 396 - key := record.Key 397 - err = db.AddPublicKey(i.Db, did, name, key, e.Commit.RKey) 398 - case jmodels.CommitOperationUpdate: 399 - l.Debug("processing update of pubkey") 400 - raw := json.RawMessage(e.Commit.Record) 401 - record := tangled.PublicKey{} 402 - err = json.Unmarshal(raw, &record) 396 + pubKey, err := models.PublicKeyFromRecord(syntax.DID(did), syntax.RecordKey(e.Commit.RKey), record) 403 397 if err != nil { 398 + l.Error("invalid record", "err", err) 399 + return err 400 + } 401 + if err := pubKey.Validate(); err != nil { 404 402 l.Error("invalid record", "err", err) 405 403 return err 406 404 } 407 405 408 - name := record.Name 409 - key := record.Key 410 - err = db.UpdatePublicKey(i.Db, did, name, key, e.Commit.RKey) 406 + err = db.UpsertPublicKey(i.Db, pubKey) 411 407 case jmodels.CommitOperationDelete: 412 408 l.Debug("processing delete of pubkey") 413 409 err = db.DeletePublicKeyByRkey(i.Db, did, e.Commit.RKey) ··· 416 412 if err != nil { 417 413 return fmt.Errorf("failed to %s pubkey record: %w", e.Commit.Operation, err) 418 414 } 415 + l.Info("processed pubkey", "operation", e.Commit.Operation, "rkey", e.Commit.RKey) 419 416 420 417 l.Info("ingested record") 421 418 return nil
+38
appview/models/pubkey.go
··· 2 2 3 3 import ( 4 4 "encoding/json" 5 + "fmt" 5 6 "time" 7 + 8 + "github.com/bluesky-social/indigo/atproto/syntax" 9 + "github.com/gliderlabs/ssh" 10 + "tangled.org/core/api/tangled" 6 11 ) 7 12 8 13 type PublicKey struct { ··· 23 28 Alias: (*Alias)(&p), 24 29 }) 25 30 } 31 + 32 + func (p *PublicKey) AsRecord() tangled.PublicKey { 33 + return tangled.PublicKey{ 34 + Name: p.Name, 35 + Key: p.Key, 36 + CreatedAt: p.Created.Format(time.RFC3339), 37 + } 38 + } 39 + 40 + var _ Validator = new(PublicKey) 41 + 42 + func (p *PublicKey) Validate() error { 43 + if _, _, _, _, err := ssh.ParseAuthorizedKey([]byte(p.Key)); err != nil { 44 + return fmt.Errorf("invalid ssh key format: %w", err) 45 + } 46 + 47 + return nil 48 + } 49 + 50 + func PublicKeyFromRecord(did syntax.DID, rkey syntax.RecordKey, record tangled.PublicKey) (PublicKey, error) { 51 + created, err := time.Parse(time.RFC3339, record.CreatedAt) 52 + if err != nil { 53 + return PublicKey{}, fmt.Errorf("invalid time format '%s'", record.CreatedAt) 54 + } 55 + 56 + return PublicKey{ 57 + Did: did.String(), 58 + Rkey: rkey.String(), 59 + Name: record.Name, 60 + Key: record.Key, 61 + Created: &created, 62 + }, nil 63 + }
+1 -1
appview/pages/templates/user/settings/fragments/keyListing.html
··· 19 19 <button 20 20 class="btn text-red-500 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300 gap-2 group" 21 21 title="Delete key" 22 - hx-delete="/settings/keys?name={{urlquery $key.Name}}&rkey={{urlquery $key.Rkey}}&key={{urlquery $key.Key}}" 22 + hx-delete="/settings/keys?name={{urlquery $key.Name}}&rkey={{urlquery $key.Rkey}}" 23 23 hx-swap="none" 24 24 hx-confirm="Are you sure you want to delete the key {{ $key.Name }}?" 25 25 >
+40 -36
appview/settings/settings.go
··· 31 31 "github.com/bluesky-social/indigo/atproto/atclient" 32 32 "github.com/bluesky-social/indigo/atproto/syntax" 33 33 lexutil "github.com/bluesky-social/indigo/lex/util" 34 - "github.com/gliderlabs/ssh" 35 34 "github.com/google/uuid" 36 35 ) 37 36 ··· 620 619 s.Logger.Warn("keys: unimplemented method") 621 620 return 622 621 case http.MethodPut: 623 - did := s.OAuth.GetDid(r) 624 - key := r.FormValue("key") 625 - key = strings.TrimSpace(key) 626 - name := r.FormValue("name") 627 - client, err := s.OAuth.AuthorizedClient(r) 628 - if err != nil { 629 - s.Pages.Notice(w, "settings-keys", "Failed to authorize. Try again later.") 630 - return 622 + created := time.Now() 623 + pubKey := models.PublicKey{ 624 + Did: s.OAuth.GetDid(r), 625 + Rkey: tid.TID(), 626 + Name: r.FormValue("name"), 627 + Key: strings.TrimSpace(r.FormValue("key")), 628 + Created: &created, 631 629 } 632 630 633 - _, _, _, _, err = ssh.ParseAuthorizedKey([]byte(key)) 634 - if err != nil { 631 + if err := pubKey.Validate(); err != nil { 635 632 s.Logger.Error("parsing public key", "err", err) 636 633 s.Pages.NoticeHTML(w, "settings-keys", "That doesn't look like a valid public key. Make sure it's a <strong>public</strong> key.") 637 634 return 638 635 } 639 636 640 - rkey := tid.TID() 641 - 642 637 tx, err := s.Db.Begin() 643 638 if err != nil { 644 639 s.Logger.Error("failed to start transaction for adding public key", "err", err) ··· 647 642 } 648 643 defer tx.Rollback() 649 644 650 - if err := db.AddPublicKey(tx, did, name, key, rkey); err != nil { 645 + if err = db.UpsertPublicKey(tx, pubKey); err != nil { 651 646 s.Logger.Error("adding public key", "err", err) 652 647 s.Pages.Notice(w, "settings-keys", "Failed to add public key.") 653 648 return 654 649 } 655 650 651 + client, err := s.OAuth.AuthorizedClient(r) 652 + if err != nil { 653 + s.Pages.Notice(w, "settings-keys", "Failed to authorize. Try again later.") 654 + return 655 + } 656 + 656 657 // store in pds too 658 + record := pubKey.AsRecord() 657 659 resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 658 660 Collection: tangled.PublicKeyNSID, 659 - Repo: did, 660 - Rkey: rkey, 661 + Repo: pubKey.Did, 662 + Rkey: pubKey.Rkey, 661 663 Record: &lexutil.LexiconTypeDecoder{ 662 - Val: &tangled.PublicKey{ 663 - CreatedAt: time.Now().Format(time.RFC3339), 664 - Key: key, 665 - Name: name, 666 - }}, 664 + Val: &record, 665 + }, 667 666 }) 668 667 // invalid record 669 668 if err != nil { ··· 690 689 691 690 name := q.Get("name") 692 691 rkey := q.Get("rkey") 693 - key := q.Get("key") 694 692 695 - s.Logger.Debug("deleting key", "name", name, "rkey", rkey, "key", key) 693 + s.Logger.Debug("deleting key", "name", name, "rkey", rkey) 696 694 697 - client, err := s.OAuth.AuthorizedClient(r) 698 - if err != nil { 699 - s.Logger.Error("failed to authorize client", "err", err) 700 - s.Pages.Notice(w, "settings-keys", "Failed to authorize client.") 701 - return 702 - } 695 + if rkey == "" { 696 + if err := db.DeletePublicKeyLegacy(s.Db, did, name); err != nil { 697 + s.Logger.Error("failed to remove public key", "err", err) 698 + s.Pages.Notice(w, "settings-keys", "Failed to remove public key.") 699 + return 700 + } 701 + } else { 702 + if err := db.DeletePublicKeyByRkey(s.Db, did, rkey); err != nil { 703 + s.Logger.Error("failed to remove public key", "err", err) 704 + s.Pages.Notice(w, "settings-keys", "Failed to remove public key.") 705 + return 706 + } 703 707 704 - if err := db.DeletePublicKey(s.Db, did, name, key); err != nil { 705 - s.Logger.Error("removing public key", "err", err) 706 - s.Pages.Notice(w, "settings-keys", "Failed to remove public key.") 707 - return 708 - } 708 + client, err := s.OAuth.AuthorizedClient(r) 709 + if err != nil { 710 + s.Logger.Error("failed to authorize client", "err", err) 711 + s.Pages.Notice(w, "settings-keys", "Failed to authorize client.") 712 + return 713 + } 709 714 710 - if rkey != "" { 711 715 // remove from pds too 712 - _, err := comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{ 716 + _, err = comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{ 713 717 Collection: tangled.PublicKeyNSID, 714 718 Repo: did, 715 719 Rkey: rkey,