forked from tangled.org/core
this repo has no description

knotserver: implement new registration flow

this flow writes a record to the owner's pds to announce knot creation

Signed-off-by: oppiliappan <me@oppi.li>

Changed files
+108 -7
appview
knotclient
knotserver
types
+13
appview/db/registration.go
···
return err
}
+
+
func RegisterV2(e Execer, domain, did string) error {
+
// TODO: this secret is useless because it is never used
+
// all comms happen through authenticated records on the firehose
+
secret := genSecret()
+
_, err := e.Exec(`
+
insert into registrations (domain, did, secret)
+
values (?, ?, ?)
+
on conflict(domain) do update set did = excluded.did, created = excluded.created, registered = excluded.created
+
`, domain, did, secret)
+
+
return err
+
}
+54 -1
appview/ingester.go
···
"encoding/json"
"fmt"
"log"
+
"strings"
"time"
"github.com/bluesky-social/indigo/atproto/syntax"
···
"github.com/ipfs/go-cid"
"tangled.sh/tangled.sh/core/api/tangled"
"tangled.sh/tangled.sh/core/appview/db"
+
"tangled.sh/tangled.sh/core/knotclient"
"tangled.sh/tangled.sh/core/rbac"
)
type Ingester func(ctx context.Context, e *models.Event) error
-
func Ingest(d db.DbWrapper, enforcer *rbac.Enforcer) Ingester {
+
func Ingest(d db.DbWrapper, enforcer *rbac.Enforcer, dev bool) Ingester {
return func(ctx context.Context, e *models.Event) error {
var err error
defer func() {
···
ingestArtifact(&d, e, enforcer)
case tangled.ActorProfileNSID:
ingestProfile(&d, e)
+
case tangled.KnotNSID:
+
ingestKnot(&d, e, dev)
}
return err
···
return nil
}
+
+
func ingestKnot(d *db.DbWrapper, e *models.Event, dev bool) error {
+
did := e.Did
+
var err error
+
+
switch e.Commit.Operation {
+
case models.CommitOperationCreate:
+
log.Println("processing knot creation")
+
raw := json.RawMessage(e.Commit.Record)
+
record := tangled.Knot{}
+
err = json.Unmarshal(raw, &record)
+
if err != nil {
+
log.Printf("invalid record: %s", err)
+
return err
+
}
+
+
host := record.Host
+
+
if strings.HasPrefix(host, "localhost") && !dev {
+
// localhost knots are not ingested except in dev mode
+
return fmt.Errorf("localhost knots not registered this appview: %s", host)
+
}
+
+
// two-way confirmation that this knot is owned by this did
+
us, err := knotclient.NewUnsignedClient(host, dev)
+
if err != nil {
+
return err
+
}
+
+
resp, err := us.Owner()
+
if err != nil {
+
return err
+
}
+
+
if resp.OwnerDid != did {
+
return fmt.Errorf("incorrect owner reported from knot %s: wanted: %s, got: %s", host, resp.OwnerDid, did)
+
}
+
+
err = db.RegisterV2(d, host, resp.OwnerDid)
+
default:
+
log.Println("this operation is not yet handled", e.Commit.Operation)
+
}
+
+
if err != nil {
+
return fmt.Errorf("failed to %s knot record: %w", e.Commit.Operation, err)
+
}
+
+
return nil
+
}
+2 -2
appview/state/state.go
···
tangled.PublicKeyNSID,
tangled.RepoArtifactNSID,
tangled.ActorProfileNSID,
+
tangled.KnotNSID,
},
nil,
slog.Default(),
···
if err != nil {
return nil, fmt.Errorf("failed to create jetstream client: %w", err)
}
-
err = jc.StartJetstream(context.Background(), appview.Ingest(wrapper, enforcer))
+
err = jc.StartJetstream(context.Background(), appview.Ingest(wrapper, enforcer, config.Core.Dev))
if err != nil {
return nil, fmt.Errorf("failed to start jetstream watcher: %w", err)
}
···
// get knots registered by this user
func (s *State) Knots(w http.ResponseWriter, r *http.Request) {
-
// for now, this is just pubkeys
user := s.oauth.GetUser(r)
registrations, err := db.RegistrationsByDid(s.db, user.Did)
if err != nil {
+14
knotclient/unsigned.go
···
return &formatPatchResponse, nil
}
+
+
func (us *UnsignedClient) Owner() (*types.KnotOwnerResponse, error) {
+
const (
+
Method = "GET"
+
Endpoint = "/owner"
+
)
+
+
req, err := us.newRequest(Method, Endpoint, nil, nil)
+
if err != nil {
+
return nil, err
+
}
+
+
return do[types.KnotOwnerResponse](us, req)
+
}
+1
knotserver/db/init.go
···
create table if not exists owner (
id integer primary key check (id = 0),
did text not null,
+
rkey text not null,
createdAt text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now'))
);
+3 -3
knotserver/db/owner.go
···
package db
-
func (d *DB) SetOwner(did string) error {
-
query := `insert into owner (id, did) values (?, ?)`
-
_, err := d.db.Exec(query, 0, did)
+
func (d *DB) SetOwner(did, rkey string) error {
+
query := `insert into owner (id, did, rkey) values (?, ?, ?)`
+
_, err := d.db.Exec(query, 0, did, rkey)
return err
}
+4 -1
knotserver/handler.go
···
// Health check. Used for two-way verification with appview.
r.With(h.VerifySignature).Get("/health", h.Health)
+
// Return did of the owner of this knot
+
r.Get("/owner", h.Owner)
+
// All public keys on the knot.
r.Get("/keys", h.Keys)
···
return err
}
-
err = h.db.SetOwner(ownerDid)
+
err = h.db.SetOwner(ownerDid, rkey)
if err != nil {
return err
}
+12
knotserver/routes.go
···
w.Write([]byte("ok"))
+
func (h *Handle) Owner(w http.ResponseWriter, r *http.Request) {
+
owner, err := h.db.Owner()
+
if err != nil {
+
writeError(w, "no owner", http.StatusNotFound)
+
return
+
}
+
+
writeJSON(w, types.KnotOwnerResponse{
+
OwnerDid: owner,
+
})
+
}
+
func validateRepoName(name string) error {
// check for path traversal attempts
if name == "." || name == ".." ||
+5
types/knot.go
···
+
package types
+
+
type KnotOwnerResponse struct {
+
OwnerDid string `json:"did"`
+
}