package contract import ( "context" "strings" "time" ) // CustomerAccessProof is a host-verified, short-lived customer request proof. // It intentionally contains no password, bearer token, card number, or device // number. The opaque SessionID can only be issued and validated by the host. type CustomerAccessProof struct { SessionID string TargetType string TargetID int64 ConversationID int64 MessageID int64 RequestID string ExpiresAt time.Time } type customerAccessProofContextKey struct{} // WithCustomerAccessProof attaches a copy of a host-verified proof to the // current request. Agent Desk propagates only these claims into async runs. func WithCustomerAccessProof(ctx context.Context, proof CustomerAccessProof) context.Context { return context.WithValue(ctx, customerAccessProofContextKey{}, proof) } // CustomerAccessProofFromContext returns only a structurally valid, live // proof. The host must still verify SessionID against its server-side store. func CustomerAccessProofFromContext(ctx context.Context) (CustomerAccessProof, bool) { proof, ok := ctx.Value(customerAccessProofContextKey{}).(CustomerAccessProof) if !ok || strings.TrimSpace(proof.SessionID) == "" || proof.TargetID <= 0 || (strings.TrimSpace(proof.TargetType) != "card" && strings.TrimSpace(proof.TargetType) != "device") || proof.ExpiresAt.IsZero() || !proof.ExpiresAt.After(time.Now()) { return CustomerAccessProof{}, false } return proof, true } // BindCustomerAccessProofToMessage binds the current request proof to the // exact persisted customer message that triggered an async Agent run. func BindCustomerAccessProofToMessage(ctx context.Context, conversationID, messageID int64, requestID string) context.Context { proof, ok := CustomerAccessProofFromContext(ctx) if !ok { return ctx } proof.ConversationID = conversationID proof.MessageID = messageID proof.RequestID = strings.TrimSpace(requestID) return WithCustomerAccessProof(ctx, proof) }