package ucp import ( "context" "net/http" "strings" ) // --------------------------------------------------------------------------- // UCP-Agent header parsing (RFC 8941 Dictionary Structured Field) // --------------------------------------------------------------------------- // ucpAgentProfileKey is the context key for the UCP-Agent profile URI. type ucpAgentProfileKey struct{} // UCPAgentProfileFromContext returns the UCP-Agent profile URI stored in the // request context, or empty string if none was advertised. func UCPAgentProfileFromContext(ctx context.Context) string { v, _ := ctx.Value(ucpAgentProfileKey{}).(string) return v } // parseUCPAgentProfile parses a single "profile" member value from an RFC 8941 // Dictionary Structured Field header. It only handles the string-value form: // // UCP-Agent: profile="https://agent.example/profiles/shopping-agent.json" // // Returns the profile URI (without surrounding quotes) or empty string. func parseUCPAgentProfile(header string) string { if header == "" { return "" } // Walk the dictionary entries looking for "profile". // RFC 8941 §3.1.2: Dict = ((im-key | key) value) *(SP "," ((im-key | key) value)) i := 0 bs := []byte(header) n := len(bs) for i < n { // Skip whitespace. for i < n && (bs[i] == ' ' || bs[i] == '\t') { i++ } if i >= n { break } // Read key (im-key or key). Lowercase letters, digits, "_", "-", "*". start := i for i < n && isKeyChar(bs[i]) { i++ } if i == start { // No key found; skip to next comma or end. for i < n && bs[i] != ',' { i++ } i++ // skip comma continue } key := string(bs[start:i]) // Check for "=" separator. RFC 8941 allows bare names (boolean true) // when "=? " follows — but we only care about key=value entries. if i < n && bs[i] == '=' { i++ // skip '=' // Read the value. For our case it's a string (starts with '"'). if i < n && bs[i] == '"' { i++ // skip opening quote valStart := i for i < n && bs[i] != '"' { if bs[i] == '\\' { i++ // skip escape } i++ } val := string(bs[valStart:i]) if i < n { i++ // skip closing quote } if strings.EqualFold(key, "profile") { return val } } } // Skip to next comma or end. for i < n && bs[i] != ',' { i++ } i++ // skip comma } return "" } // isKeyChar reports whether b is valid in an RFC 8941 key (lc-ltr, DIGIT, "_", "-", "*"). func isKeyChar(b byte) bool { return (b >= 'a' && b <= 'z') || (b >= '0' && b <= '9') || b == '_' || b == '-' || b == '*' } // WithUCPAgent is HTTP middleware that reads the UCP-Agent header from incoming // requests, parses it per RFC 8941, and stores the profile URI in the request // context so downstream handlers can inspect the calling platform's identity. // // Usage: // // mux.Handle("/ucp/v1/carts", ucp.WithUCPAgent(ucp.CartHandler(pool))) func WithUCPAgent(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if profile := parseUCPAgentProfile(r.Header.Get("UCP-Agent")); profile != "" { ctx := context.WithValue(r.Context(), ucpAgentProfileKey{}, profile) r = r.WithContext(ctx) } next.ServeHTTP(w, r) }) } // DefaultUCPAgentProfile is the profile URI sent on outgoing requests from // this commerce platform. Override via SetDefaultUCPAgentProfile or the // UCP_AGENT_PROFILE environment variable at init time. var DefaultUCPAgentProfile = "https://cart.k6n.net/.well-known/ucp" // SetDefaultUCPAgentProfile overrides the default profile URI used for outgoing // UCP-Agent headers. func SetDefaultUCPAgentProfile(profile string) { if profile != "" { DefaultUCPAgentProfile = profile } } // UCPAgentHeaderValue returns the RFC 8941 Dictionary value for the UCP-Agent // header, encoding the given profile URI. func UCPAgentHeaderValue(profile string) string { if profile == "" { profile = DefaultUCPAgentProfile } return `profile="` + profile + `"` } // WithUCPAgentOnRequest adds the UCP-Agent header to an outgoing HTTP request. // It sets the header to the default profile unless a custom profile is provided. func WithUCPAgentOnRequest(req *http.Request, profile ...string) { p := DefaultUCPAgentProfile if len(profile) > 0 && profile[0] != "" { p = profile[0] } req.Header.Set("UCP-Agent", `profile="`+p+`"`) }