~ knotless Search/
↑↓ move↵ open⇧↵ all results
0
0
main
456 lines · 13.4 KB Raw History
package trust

import (
	"context"
	"encoding/json"
	"errors"
	"slices"
	"strings"
	"sync"
	"time"

	"github.com/johnandersen777/socialweb-computer-kcp/internal/kit/opt"
	"github.com/johnandersen777/socialweb-computer-kcp/internal/kit/ttlcache"
	"github.com/johnandersen777/socialweb-computer-kcp/internal/policyengine/atproto"
	"github.com/johnandersen777/socialweb-computer-kcp/internal/policyengine/cache"
	"github.com/johnandersen777/socialweb-computer-kcp/internal/policyengine/models"
)

const (
	VouchNSID            = "sh.tangled.graph.vouch"
	BadgeBlueKeysNSID    = "com.publicdomainrelay.temp.badgeBlueKeys"
	AssociationNSID      = "com.publicdomainrelay.temp.market.association"
	BskyPostNSID         = "app.bsky.feed.post"
	DelvePostNSID        = "town.delve.feed.post"
	MentionFacetType     = "app.bsky.richtext.facet#mention"
	MirrorRKey           = "association"
	BidderAssociate      = "bidder_associate"
	RequesterAssociate   = "requester_associate"
	defaultTrustCacheTTL = 5 * time.Minute
	trustErrorTTL        = 5 * time.Second
)

type Host interface {
	Resolve(ctx context.Context, ref atproto.StrongRef) (map[string]any, error)
	ResolveOperatorDid(ctx context.Context, did string) (string, bool, error)
	Vouches(ctx context.Context, voucher, subject string) (bool, error)
	Log(level, msg string, meta map[string]any)
}

type Snapshot struct {
	OperatorOf       map[string]string   `json:"operatorOf"`
	VouchedBy        map[string][]string `json:"vouchedBy"`
	TrustedOperators []string            `json:"trustedOperators"`
}

type ResolverOptions struct {
	Client *atproto.Client
	Store  *cache.Store
	TTL    time.Duration
	Log    func(level, msg string, meta map[string]any)
}

type operatorKey struct {
	did     string
	service string
	assoc   associationKey
}

type operatorEntry struct {
	value string
	found bool
}

type vouchKey struct {
	voucher string
	subject string
}

type claim struct {
	operator string
	subject  string
	service  string
}

type Resolver struct {
	client *atproto.Client
	store  *cache.Store
	log    func(level, msg string, meta map[string]any)
	now    func() time.Time
	ops    *ttlcache.Cache[operatorKey, operatorEntry]
	vouch  *ttlcache.Cache[vouchKey, bool]
}

func NewResolver(opts ResolverOptions) *Resolver {
	client := opts.Client
	if client == nil {
		client = atproto.NewClient()
	}
	ttl := opts.TTL
	ttl = opt.Positive(ttl, defaultTrustCacheTTL)
	log := opts.Log
	if log == nil {
		log = func(string, string, map[string]any) {}
	}
	return &Resolver{
		client: client,
		store:  opts.Store,
		log:    log,
		now:    time.Now,
		ops:    ttlcache.New[operatorKey, operatorEntry](ttlcache.Options{TTL: ttl}),
		vouch:  ttlcache.New[vouchKey, bool](ttlcache.Options{TTL: ttl}),
	}
}

func (r *Resolver) Resolve(ctx context.Context, ref atproto.StrongRef) (map[string]any, error) {
	if r.store != nil {
		if value, ok := cache.ReadRecord(r.store, ref); ok {
			return value, nil
		}
	}
	value, err := r.client.GetRecord(ctx, ref)
	if err != nil {
		return nil, err
	}
	if r.store != nil {
		cache.WriteRecord(r.store, ref, value)
	}
	return value, nil
}

func (r *Resolver) getRecord(ctx context.Context, repo, collection, rkey string) (map[string]any, error) {
	value, err := r.client.GetRecord(ctx, atproto.StrongRef{URI: "at://" + repo + "/" + collection + "/" + rkey})
	if errors.Is(err, atproto.ErrRecordNotFound) {
		return nil, nil
	}
	return value, err
}

func (r *Resolver) ResolveOperatorDid(ctx context.Context, did string) (string, bool, error) {
	return r.ResolveOperatorDidFor(ctx, did, "")
}

func (r *Resolver) ResolveOperatorDidFor(ctx context.Context, did, service string) (string, bool, error) {
	return r.ResolveOperatorDidWith(ctx, did, service, Association{})
}

func (r *Resolver) ResolveOperatorDidWith(ctx context.Context, did, service string, assoc Association) (string, bool, error) {
	did = strings.TrimSpace(did)
	service = strings.TrimSpace(service)
	if did == "" {
		return "", false, nil
	}
	if !associationRole(service) {
		r.log("info", "resolveOperatorDid needs a role", map[string]any{"did": did, "service": service})
		return "", false, nil
	}
	akey := assoc.key()
	key := operatorKey{did: did, service: service, assoc: akey}
	if entry, ok := r.ops.Get(key); ok {
		return entry.value, entry.found, nil
	}
	operator, found, err := r.operatorOf(ctx, did, service, akey)
	if err != nil {
		if ctx.Err() != nil {
			return "", false, ctx.Err()
		}
		r.log("warn", "resolveOperatorDid failed", map[string]any{"did": did, "service": service, "error": err.Error()})
		r.ops.SetTTL(key, operatorEntry{}, trustErrorTTL)
		return "", false, nil
	}
	r.ops.Set(key, operatorEntry{value: operator, found: found})
	return operator, found, nil
}

func (r *Resolver) operatorOf(ctx context.Context, did, service string, assoc associationKey) (string, bool, error) {
	mirror, ok, err := r.mirrorOf(ctx, did, service)
	if err != nil || !ok {
		return "", false, err
	}
	if mirror.hasProof {
		if mirror.proof == nil {
			_, err := r.deny("the mirror proof is not a strongRef", mirror.claim, nil)
			return "", false, err
		}
		held, err := r.proofHeld(ctx, mirror.claim, *mirror.proof, assoc)
		if err != nil || !held {
			return "", false, err
		}
		return mirror.operator, true, nil
	}
	if assoc.association {
		held, err := r.associationHeld(ctx, mirror.claim, assoc)
		if err != nil {
			return "", false, err
		}
		if held {
			return mirror.operator, true, nil
		}
	}
	if assoc.claim && !assoc.requireContract {
		held, err := r.claimHeld(ctx, mirror.claim)
		if err != nil {
			return "", false, err
		}
		if held {
			return mirror.operator, true, nil
		}
	}
	r.log("info", "resolveOperatorDid unclaimed mirror", map[string]any{"did": did, "operator": mirror.operator, "service": mirror.service})
	return "", false, nil
}

type mirrorRecord struct {
	claim
	proof    *atproto.StrongRef
	hasProof bool
}

func (r *Resolver) mirrorOf(ctx context.Context, did, service string) (mirrorRecord, bool, error) {
	value, err := r.getRecord(ctx, did, BadgeBlueKeysNSID, MirrorRKey)
	if err != nil || value == nil {
		return mirrorRecord{}, false, err
	}
	held, ok := claimOf(value)
	rawProof, hasProof := value["proof"]
	proof, _ := strongRefOf(rawProof)
	mirror := mirrorRecord{claim: held, proof: proof, hasProof: hasProof}
	return mirror, ok && mirror.subject == did && mirror.operator != did && associationService(mirror.service, service), nil
}

func ClaimRKey(service, subject string) string {
	return service + "-" + strings.ReplaceAll(subject, ":", "-")
}

func (r *Resolver) claimHeld(ctx context.Context, want claim) (bool, error) {
	value, err := r.getRecord(ctx, want.operator, BadgeBlueKeysNSID, ClaimRKey(want.service, want.subject))
	if err != nil {
		return false, err
	}
	held, ok := claimOf(value)
	return ok && held == want, nil
}

func claimOf(value map[string]any) (claim, bool) {
	service, _ := value["service"].(string)
	subject, _ := value["keyId"].(string)
	operator, _ := value["challenge"].(string)
	c := claim{operator: strings.TrimSpace(operator), subject: strings.TrimSpace(subject), service: strings.TrimSpace(service)}
	return c, strings.HasPrefix(c.operator, "did:") && strings.HasPrefix(c.subject, "did:")
}

func associationRole(service string) bool {
	return service == BidderAssociate || service == RequesterAssociate
}

func associationService(recordService, want string) bool {
	return associationRole(want) && recordService == want
}

func (r *Resolver) Vouches(ctx context.Context, voucher, subject string) (bool, error) {
	voucher = strings.TrimSpace(voucher)
	subject = strings.TrimSpace(subject)
	if voucher == "" || !strings.HasPrefix(subject, "did:") {
		return false, nil
	}
	key := vouchKey{voucher: voucher, subject: subject}
	if vouched, ok := r.vouch.Get(key); ok {
		return vouched, nil
	}
	value, err := r.getRecord(ctx, voucher, VouchNSID, subject)
	if err != nil {
		if ctx.Err() != nil {
			return false, ctx.Err()
		}
		r.log("warn", "vouches failed", map[string]any{"voucher": voucher, "subject": subject, "error": err.Error()})
		return false, nil
	}
	kind, _ := value["kind"].(string)
	vouched := value != nil && kind != "denounce"
	r.vouch.Set(key, vouched)
	return vouched, nil
}

func (r *Resolver) Log(level, msg string, meta map[string]any) {
	r.log(level, msg, meta)
}

func (r *Resolver) ApplyEvent(did, rkey string) {
	r.ops.DeleteKeys(func(key operatorKey) bool { return key.did == did })
	r.vouch.DeleteKeys(func(key vouchKey) bool {
		return (did != "" && key.voucher == did) || (rkey != "" && key.voucher == rkey)
	})
}

func (r *Resolver) Invalidate() {
	r.ops.DeleteKeys(func(operatorKey) bool { return true })
	r.vouch.DeleteKeys(func(vouchKey) bool { return true })
}

type opResult struct {
	did      string
	operator string
	found    bool
	err      error
}

type vouchResult struct {
	voucher string
	subject string
	vouched bool
	err     error
}

type ServiceOperatorResolver interface {
	ResolveOperatorDidFor(ctx context.Context, did, service string) (string, bool, error)
}

type AssociationOperatorResolver interface {
	ResolveOperatorDidWith(ctx context.Context, did, service string, assoc Association) (string, bool, error)
}

func BuildSnapshotFor(ctx context.Context, host Host, selfDid, counterpartyDid, selfService, counterpartyService string) (Snapshot, error) {
	return BuildSnapshotWith(ctx, host, selfDid, counterpartyDid, selfService, counterpartyService, Association{})
}

func BuildSnapshotWith(ctx context.Context, host Host, selfDid, counterpartyDid, selfService, counterpartyService string, assoc Association) (Snapshot, error) {
	snapshot := Snapshot{
		OperatorOf:       make(map[string]string),
		VouchedBy:        make(map[string][]string),
		TrustedOperators: []string{},
	}
	if host == nil {
		return snapshot, nil
	}
	var wg sync.WaitGroup
	defer wg.Wait()
	ctx, cancel := context.WithCancel(ctx)
	defer cancel()
	roleResolver, _ := host.(ServiceOperatorResolver)
	assocResolver, _ := host.(AssociationOperatorResolver)
	selfAssoc := assoc
	selfAssoc.RequireContract = false
	resolve := func(did, service string) (string, bool, error) {
		if assocResolver != nil {
			if did == selfDid {
				return assocResolver.ResolveOperatorDidWith(ctx, did, service, selfAssoc)
			}
			return assocResolver.ResolveOperatorDidWith(ctx, did, service, assoc)
		}
		if roleResolver != nil {
			return roleResolver.ResolveOperatorDidFor(ctx, did, service)
		}
		return "", false, nil
	}
	dids := []string{selfDid}
	if counterpartyDid != "" && counterpartyDid != selfDid {
		dids = append(dids, counterpartyDid)
	}
	results := make(chan opResult, len(dids))
	for _, did := range dids {
		service := selfService
		if did == counterpartyDid {
			service = counterpartyService
		}
		wg.Go(func() {
			operator, found, err := resolve(did, service)
			results <- opResult{did: did, operator: operator, found: found, err: err}
		})
	}
	operators := make(map[string]string, len(dids))
	for range dids {
		result := <-results
		if result.err != nil {
			return snapshot, result.err
		}
		if result.did == selfDid {
			if result.found && result.operator != "" {
				operators[selfDid] = result.operator
			} else {
				operators[selfDid] = selfDid
			}
			continue
		}
		if result.found && result.operator != "" {
			operators[result.did] = result.operator
		}
	}
	snapshot.OperatorOf = operators
	trusted := []string{selfDid}
	if operator := operators[selfDid]; operator != "" && operator != selfDid {
		trusted = append(trusted, operator)
	}
	snapshot.TrustedOperators = trusted
	vouchers := distinct(append(append([]string(nil), trusted...), counterpartyDid, operators[counterpartyDid]))
	subjects := distinct([]string{selfDid, operators[selfDid], counterpartyDid, operators[counterpartyDid]})
	pairs := 0
	vouchResults := make(chan vouchResult, len(vouchers)*len(subjects))
	for _, voucher := range vouchers {
		snapshot.VouchedBy[voucher] = []string{}
		for _, subject := range subjects {
			if subject == voucher {
				continue
			}
			pairs++
			wg.Go(func() {
				vouched, err := host.Vouches(ctx, voucher, subject)
				vouchResults <- vouchResult{voucher: voucher, subject: subject, vouched: vouched, err: err}
			})
		}
	}
	for range pairs {
		result := <-vouchResults
		if result.err != nil {
			return snapshot, result.err
		}
		if result.vouched {
			snapshot.VouchedBy[result.voucher] = append(snapshot.VouchedBy[result.voucher], result.subject)
		}
	}
	for _, list := range snapshot.VouchedBy {
		slices.Sort(list)
	}
	return snapshot, nil
}

func distinct(values []string) []string {
	seen := make(map[string]bool, len(values))
	out := make([]string, 0, len(values))
	for _, value := range values {
		if value == "" || seen[value] {
			continue
		}
		seen[value] = true
		out = append(out, value)
	}
	return out
}

func (s Snapshot) ActionVouchedBy() map[string][]string {
	out := make(map[string][]string, len(s.VouchedBy))
	for voucher, list := range s.VouchedBy {
		out[voucher] = slices.Clone(list)
	}
	if len(s.TrustedOperators) == 0 {
		return out
	}
	self := s.TrustedOperators[0]
	merged := []string{}
	for _, operator := range s.TrustedOperators {
		merged = append(merged, s.VouchedBy[operator]...)
	}
	slices.Sort(merged)
	out[self] = slices.Compact(merged)
	return out
}

func (s Snapshot) CacheFiles() models.CacheEntry {
	operatorOf, _ := json.Marshal(s.OperatorOf)
	vouchedBy, _ := json.Marshal(s.ActionVouchedBy())
	trustedOperators, _ := json.Marshal(s.TrustedOperators)
	return models.CacheEntry{
		"operatorOf.json":       {Data: string(operatorOf), Encoding: models.CacheEncodingText},
		"vouchedBy.json":        {Data: string(vouchedBy), Encoding: models.CacheEncodingText},
		"trustedOperators.json": {Data: string(trustedOperators), Encoding: models.CacheEncodingText},
	}
}