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}, } }