···161161162162 return err
163163}
164164+165165+func RegisterV2(e Execer, domain, did string) error {
166166+ // TODO: this secret is useless because it is never used
167167+ // all comms happen through authenticated records on the firehose
168168+ secret := genSecret()
169169+ _, err := e.Exec(`
170170+ insert into registrations (domain, did, secret)
171171+ values (?, ?, ?)
172172+ on conflict(domain) do update set did = excluded.did, created = excluded.created, registered = excluded.created
173173+ `, domain, did, secret)
174174+175175+ return err
176176+}
+54-1
appview/ingester.go
···55 "encoding/json"
66 "fmt"
77 "log"
88+ "strings"
89 "time"
9101011 "github.com/bluesky-social/indigo/atproto/syntax"
···1314 "github.com/ipfs/go-cid"
1415 "tangled.sh/tangled.sh/core/api/tangled"
1516 "tangled.sh/tangled.sh/core/appview/db"
1717+ "tangled.sh/tangled.sh/core/knotclient"
1618 "tangled.sh/tangled.sh/core/rbac"
1719)
18201921type Ingester func(ctx context.Context, e *models.Event) error
20222121-func Ingest(d db.DbWrapper, enforcer *rbac.Enforcer) Ingester {
2323+func Ingest(d db.DbWrapper, enforcer *rbac.Enforcer, dev bool) Ingester {
2224 return func(ctx context.Context, e *models.Event) error {
2325 var err error
2426 defer func() {
···4446 ingestArtifact(&d, e, enforcer)
4547 case tangled.ActorProfileNSID:
4648 ingestProfile(&d, e)
4949+ case tangled.KnotNSID:
5050+ ingestKnot(&d, e, dev)
4751 }
48524953 return err
···285289286290 return nil
287291}
292292+293293+func ingestKnot(d *db.DbWrapper, e *models.Event, dev bool) error {
294294+ did := e.Did
295295+ var err error
296296+297297+ switch e.Commit.Operation {
298298+ case models.CommitOperationCreate:
299299+ log.Println("processing knot creation")
300300+ raw := json.RawMessage(e.Commit.Record)
301301+ record := tangled.Knot{}
302302+ err = json.Unmarshal(raw, &record)
303303+ if err != nil {
304304+ log.Printf("invalid record: %s", err)
305305+ return err
306306+ }
307307+308308+ host := record.Host
309309+310310+ if strings.HasPrefix(host, "localhost") && !dev {
311311+ // localhost knots are not ingested except in dev mode
312312+ return fmt.Errorf("localhost knots not registered this appview: %s", host)
313313+ }
314314+315315+ // two-way confirmation that this knot is owned by this did
316316+ us, err := knotclient.NewUnsignedClient(host, dev)
317317+ if err != nil {
318318+ return err
319319+ }
320320+321321+ resp, err := us.Owner()
322322+ if err != nil {
323323+ return err
324324+ }
325325+326326+ if resp.OwnerDid != did {
327327+ return fmt.Errorf("incorrect owner reported from knot %s: wanted: %s, got: %s", host, resp.OwnerDid, did)
328328+ }
329329+330330+ err = db.RegisterV2(d, host, resp.OwnerDid)
331331+ default:
332332+ log.Println("this operation is not yet handled", e.Commit.Operation)
333333+ }
334334+335335+ if err != nil {
336336+ return fmt.Errorf("failed to %s knot record: %w", e.Commit.Operation, err)
337337+ }
338338+339339+ return nil
340340+}
+2-2
appview/state/state.go
···8383 tangled.PublicKeyNSID,
8484 tangled.RepoArtifactNSID,
8585 tangled.ActorProfileNSID,
8686+ tangled.KnotNSID,
8687 },
8788 nil,
8889 slog.Default(),
···9293 if err != nil {
9394 return nil, fmt.Errorf("failed to create jetstream client: %w", err)
9495 }
9595- err = jc.StartJetstream(context.Background(), appview.Ingest(wrapper, enforcer))
9696+ err = jc.StartJetstream(context.Background(), appview.Ingest(wrapper, enforcer, config.Core.Dev))
9697 if err != nil {
9798 return nil, fmt.Errorf("failed to start jetstream watcher: %w", err)
9899 }
···408409409410// get knots registered by this user
410411func (s *State) Knots(w http.ResponseWriter, r *http.Request) {
411411- // for now, this is just pubkeys
412412 user := s.oauth.GetUser(r)
413413 registrations, err := db.RegistrationsByDid(s.db, user.Did)
414414 if err != nil {
···2929 create table if not exists owner (
3030 id integer primary key check (id = 0),
3131 did text not null,
3232+ rkey text not null,
3233 createdAt text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now'))
3334 );
3435
···157157 // Health check. Used for two-way verification with appview.
158158 r.With(h.VerifySignature).Get("/health", h.Health)
159159160160+ // Return did of the owner of this knot
161161+ r.Get("/owner", h.Owner)
162162+160163 // All public keys on the knot.
161164 r.Get("/keys", h.Keys)
162165···241244 return err
242245 }
243246244244- err = h.db.SetOwner(ownerDid)
247247+ err = h.db.SetOwner(ownerDid, rkey)
245248 if err != nil {
246249 return err
247250 }
+12
knotserver/routes.go
···12871287 w.Write([]byte("ok"))
12881288}
1289128912901290+func (h *Handle) Owner(w http.ResponseWriter, r *http.Request) {
12911291+ owner, err := h.db.Owner()
12921292+ if err != nil {
12931293+ writeError(w, "no owner", http.StatusNotFound)
12941294+ return
12951295+ }
12961296+12971297+ writeJSON(w, types.KnotOwnerResponse{
12981298+ OwnerDid: owner,
12991299+ })
13001300+}
13011301+12901302func validateRepoName(name string) error {
12911303 // check for path traversal attempts
12921304 if name == "." || name == ".." ||