diff --git a/knotserver/db/db.go b/knotserver/db/db.go --- a/knotserver/db/db.go +++ b/knotserver/db/db.go @@ -306,6 +306,27 @@ }); err != nil { return nil, err } + if err := orm.RunMigration(conn, logger, "add-rkey-to-public-keys", func(tx *sql.Tx) error { + _, mErr := tx.ExecContext(ctx, `ALTER TABLE public_keys ADD COLUMN rkey TEXT`) + return mErr + }); err != nil { + return nil, err + } + + if err := orm.RunMigration(conn, logger, "enforce-global-key-uniqueness", func(tx *sql.Tx) error { + res, mErr := tx.ExecContext(ctx, `delete from public_keys where id not in (select min(id) from public_keys group by key)`) + if mErr != nil { + return mErr + } + if n, rErr := res.RowsAffected(); rErr == nil && n > 0 { + logger.Warn("dropped duplicate public keys to enforce global key uniqueness", "deleted", n) + } + _, mErr = tx.ExecContext(ctx, `create unique index if not exists idx_public_keys_key on public_keys(key)`) + return mErr + }); err != nil { + return nil, err + } + return &DB{ db: db, logger: logger, diff --git a/knotserver/db/pubkeys.go b/knotserver/db/pubkeys.go --- a/knotserver/db/pubkeys.go +++ b/knotserver/db/pubkeys.go @@ -1,86 +1,118 @@ package db import ( + "database/sql" + "log/slog" "strconv" "time" + "github.com/bluesky-social/indigo/atproto/syntax" "tangled.org/core/api/tangled" ) type PublicKey struct { - Did string + Did syntax.DID + Rkey syntax.RecordKey tangled.PublicKey } -func (d *DB) AddPublicKeyFromRecord(did string, recordIface map[string]interface{}) error { - record := make(map[string]string) - for k, v := range recordIface { - if str, ok := v.(string); ok { - record[k] = str +func (d *DB) UpsertPublicKey(pk PublicKey) error { + tx, err := d.db.Begin() + if err != nil { + return err + } + defer tx.Rollback() + + if pk.Rkey != "" { + if _, err := tx.Exec(`delete from public_keys where did = ? and rkey = ?`, pk.Did, pk.Rkey); err != nil { + return err } } - pk := PublicKey{ - Did: did, + if err := insertPublicKey(tx, d.logger, pk); err != nil { + return err } - pk.Key = record["key"] - pk.CreatedAt = record["createdAt"] - return d.AddPublicKey(pk) + return tx.Commit() } -func (d *DB) AddPublicKey(pk PublicKey) error { +func insertPublicKey(tx *sql.Tx, logger *slog.Logger, pk PublicKey) error { + if pk.Key == "" { + logger.Warn("skipping public key with empty key value", "did", pk.Did, "rkey", pk.Rkey) + return nil + } + if pk.CreatedAt == "" { pk.CreatedAt = time.Now().Format(time.RFC3339) } - query := `insert or ignore into public_keys (did, key, created) values (?, ?, ?)` - _, err := d.db.Exec(query, pk.Did, pk.Key, pk.CreatedAt) - return err + res, err := tx.Exec( + `insert or ignore into public_keys (did, key, rkey, created) values (?, ?, ?, ?)`, + pk.Did, pk.Key, pk.Rkey, pk.CreatedAt, + ) + if err != nil { + return err + } + + if rows, err := res.RowsAffected(); err == nil && rows == 0 { + logger.Warn("public key not stored, already registered to another did", "did", pk.Did, "rkey", pk.Rkey) + } + + return nil } -func (d *DB) RemovePublicKey(did string) error { - query := `delete from public_keys where did = ?` - _, err := d.db.Exec(query, did) +func (d *DB) DeletePublicKeyByRkey(did syntax.DID, rkey syntax.RecordKey) error { + if rkey == "" { + return nil + } + + query := `delete from public_keys where did = ? and rkey = ?` + _, err := d.db.Exec(query, did, rkey) return err } -func (pk *PublicKey) JSON() map[string]any { - return map[string]any{ - "did": pk.Did, - "key": pk.Key, - "createdAt": pk.CreatedAt, +func (d *DB) ReplacePublicKeys(did syntax.DID, keys []PublicKey) error { + tx, err := d.db.Begin() + if err != nil { + return err } -} + defer tx.Rollback() -func (d *DB) GetAllPublicKeys() ([]PublicKey, error) { - var keys []PublicKey + if _, err := tx.Exec(`delete from public_keys where did = ?`, did); err != nil { + return err + } - rows, err := d.db.Query(`select key, did, created from public_keys`) - if err != nil { - return nil, err + if err := insertPublicKeys(tx, d.logger, keys); err != nil { + return err } - defer rows.Close() - for rows.Next() { - var publicKey PublicKey - if err := rows.Scan(&publicKey.Key, &publicKey.Did, &publicKey.CreatedAt); err != nil { - return nil, err - } - keys = append(keys, publicKey) + return tx.Commit() +} + +func insertPublicKeys(tx *sql.Tx, logger *slog.Logger, keys []PublicKey) error { + if len(keys) == 0 { + return nil } - if err := rows.Err(); err != nil { - return nil, err + if err := insertPublicKey(tx, logger, keys[0]); err != nil { + return err } - return keys, nil + return insertPublicKeys(tx, logger, keys[1:]) +} + +func (pk *PublicKey) JSON() map[string]any { + return map[string]any{ + "did": pk.Did, + "key": pk.Key, + "createdAt": pk.CreatedAt, + } } -func (d *DB) GetPublicKeys(did string) ([]PublicKey, error) { +func (d *DB) GetAllPublicKeys() ([]PublicKey, error) { var keys []PublicKey - rows, err := d.db.Query(`select did, key, created from public_keys where did = ?`, did) + rows, err := d.db.Query(`select key, did, created from public_keys`) if err != nil { return nil, err } @@ -88,7 +120,7 @@ defer rows.Close() for rows.Next() { var publicKey PublicKey - if err := rows.Scan(&publicKey.Did, &publicKey.Key, &publicKey.CreatedAt); err != nil { + if err := rows.Scan(&publicKey.Key, &publicKey.Did, &publicKey.CreatedAt); err != nil { return nil, err } keys = append(keys, publicKey) diff --git a/knotserver/db/pubkeys_test.go b/knotserver/db/pubkeys_test.go new file mode 100644 --- /dev/null +++ b/knotserver/db/pubkeys_test.go @@ -0,0 +1,136 @@ +package db + +import ( + "testing" + + "github.com/bluesky-social/indigo/atproto/syntax" + "tangled.org/core/api/tangled" +) + +const ( + didBoltless = "did:plc:boltless" + didAkshay = "did:plc:akshay" + keyShared = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAISharedSharedSharedSharedSharedSharedShar01" + keyRotated = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIRotatedRotatedRotatedRotatedRotatedRot02" + keyOther = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOtherOtherOtherOtherOtherOtherOtherOth03" +) + +func TestUpsertPublicKey_GlobalUniqueness(t *testing.T) { + d := newTestDB(t) + addDid(t, d, didBoltless) + addDid(t, d, didAkshay) + + if err := d.UpsertPublicKey(pubKey(didBoltless, "r-first", keyShared)); err != nil { + t.Fatalf("first upsert: %v", err) + } + if err := d.UpsertPublicKey(pubKey(didAkshay, "r-second", keyShared)); err != nil { + t.Fatalf("second upsert: %v", err) + } + + owners := ownersByKey(t, d) + if got := len(owners); got != 1 { + t.Fatalf("stored %d copies of the key, want 1, owners=%v", got, owners) + } + if owners[keyShared] != didBoltless { + t.Errorf("key registered to %q, want %q (first writer keeps it)", owners[keyShared], didBoltless) + } +} + +func TestUpsertPublicKey_RotatesAtSameRkey(t *testing.T) { + d := newTestDB(t) + addDid(t, d, didBoltless) + + if err := d.UpsertPublicKey(pubKey(didBoltless, "rotate", keyShared)); err != nil { + t.Fatalf("upsert old: %v", err) + } + if err := d.UpsertPublicKey(pubKey(didBoltless, "rotate", keyRotated)); err != nil { + t.Fatalf("upsert new: %v", err) + } + + owners := ownersByKey(t, d) + if _, ok := owners[keyShared]; ok { + t.Errorf("old key %q survived rotation at the same rkey", keyShared) + } + if _, ok := owners[keyRotated]; !ok { + t.Errorf("rotated key %q not stored", keyRotated) + } + if got := len(owners); got != 1 { + t.Errorf("stored %d keys, want 1", got) + } +} + +func TestDeletePublicKeyByRkey(t *testing.T) { + d := newTestDB(t) + addDid(t, d, didBoltless) + + if err := d.UpsertPublicKey(pubKey(didBoltless, "keep", keyShared)); err != nil { + t.Fatalf("upsert keep: %v", err) + } + if err := d.UpsertPublicKey(pubKey(didBoltless, "drop", keyOther)); err != nil { + t.Fatalf("upsert drop: %v", err) + } + + if err := d.DeletePublicKeyByRkey(didBoltless, ""); err != nil { + t.Fatalf("delete with empty rkey: %v", err) + } + if got := len(ownersByKey(t, d)); got != 2 { + t.Fatalf("empty rkey deleted %d rows, want a no-op leaving 2", 2-got) + } + + if err := d.DeletePublicKeyByRkey(didBoltless, "drop"); err != nil { + t.Fatalf("delete drop: %v", err) + } + + owners := ownersByKey(t, d) + if _, ok := owners[keyOther]; ok { + t.Errorf("key at rkey %q was not deleted", "drop") + } + if _, ok := owners[keyShared]; !ok { + t.Errorf("delete removed the wrong key, %q is gone", keyShared) + } +} + +func TestInsertPublicKey_SkipsEmptyKey(t *testing.T) { + d := newTestDB(t) + addDid(t, d, didBoltless) + + if err := d.UpsertPublicKey(pubKey(didBoltless, "empty", "")); err != nil { + t.Fatalf("upsert empty key: %v", err) + } + + if got := len(ownersByKey(t, d)); got != 0 { + t.Errorf("stored %d rows for an empty key, want 0", got) + } +} + +func addDid(t *testing.T, d *DB, did string) { + t.Helper() + if err := AddDid(d, did); err != nil { + t.Fatalf("AddDid(%q): %v", did, err) + } +} + +func pubKey(did syntax.DID, rkey syntax.RecordKey, key string) PublicKey { + return PublicKey{ + Did: did, + Rkey: rkey, + PublicKey: tangled.PublicKey{Key: key, CreatedAt: "2026-06-20T00:00:00Z"}, + } +} + +func ownersByKey(t *testing.T, d *DB) map[string]string { + t.Helper() + rows, err := d.GetAllPublicKeys() + if err != nil { + t.Fatalf("GetAllPublicKeys: %v", err) + } + return foldOwners(rows, map[string]string{}) +} + +func foldOwners(rows []PublicKey, acc map[string]string) map[string]string { + if len(rows) == 0 { + return acc + } + acc[rows[0].Key] = rows[0].Did.String() + return foldOwners(rows[1:], acc) +} diff --git a/knotserver/ingester.go b/knotserver/ingester.go --- a/knotserver/ingester.go +++ b/knotserver/ingester.go @@ -26,24 +26,33 @@ "tangled.org/core/workflow" ) func (h *Knot) processPublicKey(ctx context.Context, event *jmodels.Event) error { - l := log.FromContext(ctx) - raw := json.RawMessage(event.Commit.Record) - did := event.Did + l := log.FromContext(ctx).With("handler", "processPublicKey", "did", event.Did, "rkey", event.Commit.RKey) + did := syntax.DID(event.Did) + rkey := syntax.RecordKey(event.Commit.RKey) - var record tangled.PublicKey - if err := json.Unmarshal(raw, &record); err != nil { - return fmt.Errorf("failed to unmarshal record: %w", err) + switch event.Commit.Operation { + case jmodels.CommitOperationCreate, jmodels.CommitOperationUpdate: + var record tangled.PublicKey + if err := json.Unmarshal(json.RawMessage(event.Commit.Record), &record); err != nil { + return fmt.Errorf("failed to unmarshal record: %w", err) + } + + pk := db.PublicKey{ + Did: did, + Rkey: rkey, + PublicKey: record, + } + if err := h.db.UpsertPublicKey(pk); err != nil { + return fmt.Errorf("failed to upsert public key: %w", err) + } + l.Info("upserted public key from firehose") + case jmodels.CommitOperationDelete: + if err := h.db.DeletePublicKeyByRkey(did, rkey); err != nil { + return fmt.Errorf("failed to delete public key: %w", err) + } + l.Info("deleted public key from firehose") } - pk := db.PublicKey{ - Did: did, - PublicKey: record, - } - if err := h.db.AddPublicKey(pk); err != nil { - l.Error("failed to add public key", "error", err) - return fmt.Errorf("failed to add public key: %w", err) - } - l.Info("added public key from firehose", "did", did) return nil } diff --git a/knotserver/keys/keys.go b/knotserver/keys/keys.go --- a/knotserver/keys/keys.go +++ b/knotserver/keys/keys.go @@ -3,6 +3,7 @@ import ( "context" "fmt" + "log/slog" comatproto "github.com/bluesky-social/indigo/api/atproto" "github.com/bluesky-social/indigo/atproto/identity" @@ -13,10 +14,15 @@ "tangled.org/core/knotserver/db" "tangled.org/core/log" ) -func FetchAndStore(ctx context.Context, dir identity.Directory, store *db.DB, did string) error { +const ( + publicKeyPageSize = 100 + maxPublicKeyPages = 20 +) + +func FetchAndStore(ctx context.Context, dir identity.Directory, store *db.DB, did syntax.DID) error { l := log.FromContext(ctx) - id, err := dir.LookupDID(ctx, syntax.DID(did)) + id, err := dir.LookupDID(ctx, did) if err != nil { return fmt.Errorf("lookup did to fetch keys: %w", err) } @@ -28,22 +34,85 @@ return nil } xrpcc := indigoxrpc.Client{Host: serviceEndpoint.URL} - resp, err := comatproto.RepoListRecords(ctx, &xrpcc, tangled.PublicKeyNSID, "", 50, did, false) + records, err := listAllPublicKeys(ctx, l, &xrpcc, did, "", maxPublicKeyPages) if err != nil { return fmt.Errorf("fetching public keys for did: %w", err) } - for _, record := range resp.Records { - if record == nil { - continue - } - key, ok := record.Value.Val.(*tangled.PublicKey) - if !ok || key == nil { - continue - } - if err := store.AddPublicKey(db.PublicKey{Did: did, PublicKey: *key}); err != nil { - return fmt.Errorf("adding public key to db: %w", err) - } + keys := collectPublicKeys(l, did, records) + if len(keys) == 0 { + l.Warn("no public keys fetched, skipping replace so existing keys are not wiped by a transient empty response", "did", did) + return nil + } + + if err := store.ReplacePublicKeys(did, keys); err != nil { + return fmt.Errorf("replacing public keys in db: %w", err) } return nil } + +func listAllPublicKeys(ctx context.Context, l *slog.Logger, xrpcc *indigoxrpc.Client, did syntax.DID, cursor string, pagesLeft int) ([]*comatproto.RepoListRecords_Record, error) { + if pagesLeft <= 0 { + l.Warn("public key pagination hit page cap, remaining keys ignored", "did", did, "cap", maxPublicKeyPages) + return nil, nil + } + + resp, err := comatproto.RepoListRecords(ctx, xrpcc, tangled.PublicKeyNSID, cursor, publicKeyPageSize, did.String(), false) + if err != nil { + return nil, err + } + + if resp.Cursor == nil || *resp.Cursor == "" || len(resp.Records) == 0 { + return resp.Records, nil + } + + rest, err := listAllPublicKeys(ctx, l, xrpcc, did, *resp.Cursor, pagesLeft-1) + if err != nil { + return nil, err + } + + return append(resp.Records, rest...), nil +} + +func collectPublicKeys(l *slog.Logger, did syntax.DID, records []*comatproto.RepoListRecords_Record) []db.PublicKey { + return collectPublicKeysInto(l, did, records, nil) +} + +func collectPublicKeysInto(l *slog.Logger, did syntax.DID, records []*comatproto.RepoListRecords_Record, acc []db.PublicKey) []db.PublicKey { + if len(records) == 0 { + return acc + } + + return collectPublicKeysInto(l, did, records[1:], appendValidKey(l, did, acc, records[0])) +} + +func appendValidKey(l *slog.Logger, did syntax.DID, acc []db.PublicKey, record *comatproto.RepoListRecords_Record) []db.PublicKey { + if record == nil { + return acc + } + + key, ok := record.Value.Val.(*tangled.PublicKey) + if !ok || key == nil { + return acc + } + + rkey, err := recordKeyFromURI(record.Uri) + if err != nil { + l.Warn("skipping public key with unparseable uri", "uri", record.Uri, "err", err) + return acc + } + + return append(acc, db.PublicKey{ + Did: did, + Rkey: rkey, + PublicKey: *key, + }) +} + +func recordKeyFromURI(uri string) (syntax.RecordKey, error) { + aturi, err := syntax.ParseATURI(uri) + if err != nil { + return "", err + } + return aturi.RecordKey(), nil +} diff --git a/knotserver/keys/keys_test.go b/knotserver/keys/keys_test.go new file mode 100644 --- /dev/null +++ b/knotserver/keys/keys_test.go @@ -0,0 +1,199 @@ +package keys + +import ( + "context" + "encoding/json" + "errors" + "net/http" + "net/http/httptest" + "path/filepath" + "testing" + + comatproto "github.com/bluesky-social/indigo/api/atproto" + "github.com/bluesky-social/indigo/atproto/identity" + "github.com/bluesky-social/indigo/atproto/syntax" + lexutil "github.com/bluesky-social/indigo/lex/util" + "tangled.org/core/api/tangled" + "tangled.org/core/knotserver/db" +) + +const ( + didBoltless = "did:plc:boltless" + keyAlpha = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAlphaAlphaAlphaAlphaAlphaAlphaAlphaAlpha01" + keyBravo = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIABravoBravoBravoBravoBravoBravoBravoBravo02" +) + +func TestFetchAndStore_EmptyResponseDoesNotWipe(t *testing.T) { + store := newKeyStore(t) + seedKey(t, store, didBoltless, "seed", keyAlpha) + + srv := pdsServer(t, map[string]*comatproto.RepoListRecords_Output{ + "": page(""), + }) + defer srv.Close() + + if err := FetchAndStore(context.Background(), fakeDirectory{pdsURL: srv.URL}, store, didBoltless); err != nil { + t.Fatalf("FetchAndStore: %v", err) + } + + owners := ownersByKey(t, store) + if _, ok := owners[keyAlpha]; !ok { + t.Fatalf("seeded key was wiped by an empty fetch response, owners=%v", owners) + } + if got := len(owners); got != 1 { + t.Errorf("stored %d keys, want 1", got) + } +} + +func TestFetchAndStore_ReplacesExistingKeys(t *testing.T) { + store := newKeyStore(t) + seedKey(t, store, didBoltless, "stale", keyAlpha) + + srv := pdsServer(t, map[string]*comatproto.RepoListRecords_Output{ + "": page("", pubkeyRecord(didBoltless, "fresh", keyBravo)), + }) + defer srv.Close() + + if err := FetchAndStore(context.Background(), fakeDirectory{pdsURL: srv.URL}, store, didBoltless); err != nil { + t.Fatalf("FetchAndStore: %v", err) + } + + owners := ownersByKey(t, store) + if _, ok := owners[keyAlpha]; ok { + t.Errorf("stale key %q survived a full replace", keyAlpha) + } + if _, ok := owners[keyBravo]; !ok { + t.Errorf("fresh key %q was not stored", keyBravo) + } + if got := len(owners); got != 1 { + t.Errorf("stored %d keys, want 1", got) + } +} + +func TestFetchAndStore_PaginatesAcrossPages(t *testing.T) { + store := newKeyStore(t) + if err := db.AddDid(store, didBoltless); err != nil { + t.Fatalf("AddDid: %v", err) + } + + srv := pdsServer(t, map[string]*comatproto.RepoListRecords_Output{ + "": page("next", pubkeyRecord(didBoltless, "r1", keyAlpha)), + "next": page("", pubkeyRecord(didBoltless, "r2", keyBravo)), + }) + defer srv.Close() + + if err := FetchAndStore(context.Background(), fakeDirectory{pdsURL: srv.URL}, store, didBoltless); err != nil { + t.Fatalf("FetchAndStore: %v", err) + } + + owners := ownersByKey(t, store) + if _, ok := owners[keyAlpha]; !ok { + t.Errorf("first-page key %q missing", keyAlpha) + } + if _, ok := owners[keyBravo]; !ok { + t.Errorf("second-page key %q missing", keyBravo) + } + if got := len(owners); got != 2 { + t.Errorf("stored %d keys, want 2", got) + } +} + +func newKeyStore(t *testing.T) *db.DB { + t.Helper() + store, err := db.Setup(context.Background(), filepath.Join(t.TempDir(), "test.db")) + if err != nil { + t.Fatalf("db.Setup: %v", err) + } + return store +} + +func seedKey(t *testing.T, store *db.DB, did syntax.DID, rkey syntax.RecordKey, key string) { + t.Helper() + if err := db.AddDid(store, did.String()); err != nil { + t.Fatalf("AddDid: %v", err) + } + if err := store.UpsertPublicKey(db.PublicKey{ + Did: did, + Rkey: rkey, + PublicKey: tangled.PublicKey{Key: key, CreatedAt: "2026-06-20T00:00:00Z"}, + }); err != nil { + t.Fatalf("UpsertPublicKey: %v", err) + } +} + +func ownersByKey(t *testing.T, store *db.DB) map[string]string { + t.Helper() + rows, err := store.GetAllPublicKeys() + if err != nil { + t.Fatalf("GetAllPublicKeys: %v", err) + } + return foldOwners(rows, map[string]string{}) +} + +func foldOwners(rows []db.PublicKey, acc map[string]string) map[string]string { + if len(rows) == 0 { + return acc + } + acc[rows[0].Key] = rows[0].Did.String() + return foldOwners(rows[1:], acc) +} + +func page(cursor string, records ...*comatproto.RepoListRecords_Record) *comatproto.RepoListRecords_Output { + out := &comatproto.RepoListRecords_Output{Records: records} + if cursor != "" { + out.Cursor = &cursor + } + return out +} + +func pubkeyRecord(did, rkey, key string) *comatproto.RepoListRecords_Record { + return &comatproto.RepoListRecords_Record{ + Uri: "at://" + did + "/" + tangled.PublicKeyNSID + "/" + rkey, + Cid: "bafyreib2rxk3rybk3aobmv5cjuql3bm2twh4jo5uxgr3rxbd3xrnp5z5cy", + Value: &lexutil.LexiconTypeDecoder{ + Val: &tangled.PublicKey{Key: key, CreatedAt: "2026-06-20T00:00:00Z"}, + }, + } +} + +func pdsServer(t *testing.T, pages map[string]*comatproto.RepoListRecords_Output) *httptest.Server { + t.Helper() + return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + cursor := r.URL.Query().Get("cursor") + out, ok := pages[cursor] + if !ok { + t.Errorf("listRecords requested with unexpected cursor %q", cursor) + http.Error(w, "no such page", http.StatusInternalServerError) + return + } + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(out); err != nil { + t.Errorf("encoding listRecords response: %v", err) + } + })) +} + +type fakeDirectory struct { + pdsURL string +} + +func (f fakeDirectory) LookupDID(ctx context.Context, did syntax.DID) (*identity.Identity, error) { + return &identity.Identity{ + DID: did, + Services: map[string]identity.ServiceEndpoint{ + "atproto_pds": {Type: "AtprotoPersonalDataServer", URL: f.pdsURL}, + }, + }, nil +} + +func (f fakeDirectory) LookupHandle(ctx context.Context, handle syntax.Handle) (*identity.Identity, error) { + return nil, errors.New("LookupHandle unused in tests") +} + +func (f fakeDirectory) Lookup(ctx context.Context, atid syntax.AtIdentifier) (*identity.Identity, error) { + return nil, errors.New("Lookup unused in tests") +} + +func (f fakeDirectory) Purge(ctx context.Context, atid syntax.AtIdentifier) error { + return nil +} diff --git a/knotserver/router.go b/knotserver/router.go --- a/knotserver/router.go +++ b/knotserver/router.go @@ -9,6 +9,7 @@ "net/http" "strings" "sync" + "github.com/bluesky-social/indigo/atproto/syntax" "github.com/go-chi/chi/v5" "tangled.org/core/idresolver" "tangled.org/core/jetstream" @@ -217,7 +218,7 @@ if err := h.e.AddKnotOwner(rbacDomain, cfgOwner); err != nil { return fmt.Errorf("failed to add owner to RBAC: %w", err) } - err = keys.FetchAndStore(ctx, h.resolver.Directory(), h.db, cfgOwner) + err = keys.FetchAndStore(ctx, h.resolver.Directory(), h.db, syntax.DID(cfgOwner)) if err != nil { h.l.Error("fetching and adding owners public keys", "error", err, "did", cfgOwner) } diff --git a/knotserver/xrpc/acl_saga.go b/knotserver/xrpc/acl_saga.go --- a/knotserver/xrpc/acl_saga.go +++ b/knotserver/xrpc/acl_saga.go @@ -188,7 +188,7 @@ func (h *Xrpc) fetchKeysAsync(ctx context.Context, l *slog.Logger, subject syntax.DID) { kctx, cancel := context.WithTimeout(context.WithoutCancel(ctx), keyFetchTimeout) go func() { defer cancel() - if err := keys.FetchAndStore(kctx, h.Resolver.Directory(), h.Db, subject.String()); err != nil { + if err := keys.FetchAndStore(kctx, h.Resolver.Directory(), h.Db, subject); err != nil { l.Warn("failed to fetch subject public keys, continuing", "subject", subject, "error", err) } }() diff --git a/knotserver/xrpc/list_keys.go b/knotserver/xrpc/list_keys.go --- a/knotserver/xrpc/list_keys.go +++ b/knotserver/xrpc/list_keys.go @@ -31,7 +31,7 @@ publicKeys := make([]*tangled.KnotListKeys_PublicKey, 0, len(keys)) for _, key := range keys { publicKeys = append(publicKeys, &tangled.KnotListKeys_PublicKey{ - Did: key.Did, + Did: key.Did.String(), Key: key.Key, CreatedAt: key.CreatedAt, })